Drilldown cloning (#91959)

* refactor: 💡 remove switch statement

* feat: 🎸 improve <ListManageDrilldowns> stories

* refactor: 💡 remove <FlyoutListManageDrilldowns> component

* refactor: 💡 simplify <FlyoutDrilldownWizard> component

* feat: 🎸 introduce React context for drilldowns flyout

* refactor: 💡 rename drilldown manager component

* refactor: 💡 rename drilldown manager in example plugin

* refactor: 💡 rename folder to "containers"

* feat: 🎸 use drilldown context to store UI state

* chore: 🤖 fix linter errors

* refactor: 💡 move Drilldown Manager into its own folder

* feat: 🎸 add drilldown state management

* feat: 🎸 add <PresentablePicker> component

* chore: 🤖 clean up component props

* feat: 🎸 add <ActionFactoryPicker> component

* feat: 🎸 connect action factory picker component

* chore: 🤖 cleanup

* feat: 🎸 start work on <DrilldownForm>

* feat: 🎸 add <TriggerPicker> component

* test: 💍 add <TriggerPicker> stories

* feat: 🎸 add <DrilldownForm> component

* refactor: 💡 remove ActionFactory from <DrilldownForm>

* feat: 🎸 connect <DrilldownForm> component

* feat: 🎸 improve new drilldown connected form

* fix: 🐛 correct TypeScript types

* feat: 🎸 show trigger intersection in the UI

* feat: 🎸 add <ActionFactory> component

* feat: 🎸 show connected <ActionFactoryView> component

* feat: 🎸 use a single action factory control

* fix: 🐛 remove unused props

* refactor: 💡 improve create drilldown form

* fix: 🐛 correct hello bar close callback

* feat: 🎸 connect welcome message state to local storage

* fix: 🐛 correct TypeScript errors

* fix: 🐛 hide correctly hello bar

* feat: 🎸 add drilldown creation logic

* feat: 🎸 connect event list state

* fix: 🐛 correct story props

* fix: 🐛 correct typescript errors

* feat: 🎸 navigate to list view when drilldown is created

* feat: 🎸 add EUI tabs

* feat: 🎸 connect route state to tabs

* refactor: 💡 move flyout content into a separate component

* refactor: 💡 move manager tabs into a separate component

* chore: 🤖 delete unused variables

* feat: 🎸 make drilldowns title dynamic

* docs: ✏️ update README

* feat: 🎸 add <DrilldownManagerTitle> component

* feat: 🎸 make flyout footer dynamic

* refactor: 💡 make action factory state depend on the route

* feat: 🎸 create standalone new drilldown form

* fix: 🐛 support i18n in new drilldown form

* feat: 🎸 add space between action factory picker

* refactor: 💡 simplify <DrilldownManagerContent>

* feat: 🎸 add back chevron button to drilldown flyout

* chore: 🤖 remove unused translations

* chore: 🤖 delete unused translation file

* feat: 🎸 add <DrilldownManagerFooter> and improve title comp

* feat: 🎸 improve <CreateDrilldownForm>

* refactor: 💡 improve variable name

* feat: 🎸 add isValid() to drilldown state

* refactor: 💡 move drilldown hello bar into a seprate component

* feat: 🎸 show drilldown list on "Manage" tab

* refactor: 💡 improve drilldown list component

* feat: 🎸 connect drill list comp to delete and edit functional

* feat: 🎸 support form disabling when drill creation in progress

* feat: 🎸 connect drilldown edit form

* feat: 🎸 use /manage route for editing drilldowns

* fix: 🐛 render constant action factory in edit mode

* refactor: 💡 move drilldown footer submit button in a sep comp

* feat: 🎸 connect editing to drilldown edit form

* feat: 🎸 disable edit buttons when list has selection

* chore: 🤖 remove unused code

* feat: 🎸 show error if delete fails

* chore: 🤖 remove legacy code

* feat: 🎸 improve drilldown table component

* fix: 🐛 fix "Trigger" translation

* fix: 🐛 import correct drilldown table item

* feat: 🎸 handle empty name errors

* feat: 🎸 track config error in drilldown state

* feat: 🎸 track drilldown state trigger errors

* fix: 🐛 pre-select trigger if only one trigger is available

* feat: 🎸 add drilldown template interface

* feat: 🎸 progress on drilldown cloning list

* feat: 🎸 use table view to display drilldown cloning templates

* feat: 🎸 generate drilldown templates from embeddable siblings

* feat: 🎸 improve drilldown template generation logic

* feat: 🎸 improve drilldown template preview

* feat: 🎸 improve start from template table view

* feat: 🎸 show number of items to be cloned

* feat: 🎸 add ID to drilldown templates

* feat: 🎸 implement basic cloning functionality

* feat: 🎸 add ability to create drilldown from existing one

* feat: 🎸 improve cloning behaviour

* feat: 🎸 improve icon of templates

* feat: 🎸 remove "Start from template" accordion

* feat: 🎸 improve cloning table view

* feat: 🎸 add "Triggers" column

* feat: 🎸 enable drilldown actions in "edit" mode

* fix: 🐛 reset drilldown state cache by factory on new creation

* refactor: 💡 use in-memory table for drilldown template list

* feat: 🎸 add search box to drilldown template in-memory table

* feat: 🎸 show back chevron on no steps back, to close flyout

* feat: 🎸 show notification when drilldown was just cloned

* feat: 🎸 add i18n to cloning notification, and move to sep comp

* feat: 🎸 add support for plural and singular in translation

* feat: 🎸 make new drilldown names unique with (copy x) at end

* feat: 🎸 rename cloning to copying

* feat: 🎸 show incompatible trigger warning in template list

* refactor: 💡 craete text with icon component

* feat: 🎸 show trigger warning in manage view

* refactor: 💡 use <TextWithIcon> in drilldown table view

* feat: 🎸 add <TriggerLineItem> component

* feat: 🎸 show tooltip on incompatible icon hover

* feat: 🎸 unify tooltips in <TextWithIcon> component

* feat: 🎸 enable sorting in drilldown tables

* fix: 🐛 correct ui actions x-pack typescript errors

* fix: 🐛 correct ui actions example plugin errors

* docs: ✏️ update autogenerated docs

* chore: 🤖 remove unused translations

* test: 💍 remove implementation from todo placeholders

* test: 💍 fix dashboard drilldown functional test

* chore: 🤖 remove unused constants

* test: 💍 fix url drilldown functional test

* test: 💍 add drilldown manager state tests

* feat: 🎸 dont show back button when no more steps back

* feat: 🎸 pre-fill in drilldown name

* feat: 🎸 add copy suffix when creating new drilldown

* feat: 🎸 close flyout if opened in create mode

* feat: 🎸 dont close flyout if user navigated to "manage" tab

* feat: 🎸 show smaller notification message on cloning

* feat: 🎸 add "(copy X)" text when cloning/copying drilldowns

* feat: 🎸 make copy prefixes start from "(copy)"

* feat: 🎸 add link to dismiss notification

* feat: 🎸 rename "Clone (x)" button to "Copy (x)"

* feat: 🎸 rename tempalte table last column copy button

* feat: 🎸 use "copied" terminology in notification

* feat: 🎸 add "Copy existing drilldown" label to template picker

* feat: 🎸 use "auto" layout on drilldown template picker table

* test: 💍 add drilldown manager tests

* feat: 🎸 swap label for title
This commit is contained in:
Vadim Dalecky 2021-04-19 19:26:05 +02:00 committed by GitHub
parent 60ee22361f
commit 77fe59d58f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
131 changed files with 3933 additions and 1884 deletions

View file

@ -7,7 +7,7 @@
<b>Signature:</b>
```typescript
protected readonly children: {
readonly children: {
[key: string]: IEmbeddable<any, any> | ErrorEmbeddable;
};
```

View file

@ -9,7 +9,7 @@ Returns tooltip text which should be displayed when user hovers this object. Sho
<b>Signature:</b>
```typescript
getDisplayNameTooltip(context: Context): string;
getDisplayNameTooltip?(context: Context): string;
```
## Parameters

View file

@ -32,7 +32,7 @@ export abstract class Container<
extends Embeddable<TContainerInput, TContainerOutput>
implements IContainer<TChildInput, TContainerInput, TContainerOutput> {
public readonly isContainer: boolean = true;
protected readonly children: {
public readonly children: {
[key: string]: IEmbeddable<any, any> | ErrorEmbeddable;
} = {};

View file

@ -160,7 +160,7 @@ export abstract class Container<TChildInput extends Partial<EmbeddableInput> = {
// (undocumented)
addNewEmbeddable<EEI extends EmbeddableInput = EmbeddableInput, EEO extends EmbeddableOutput = EmbeddableOutput, E extends IEmbeddable<EEI, EEO> = IEmbeddable<EEI, EEO>>(type: string, explicitInput: Partial<EEI>): Promise<E | ErrorEmbeddable>;
// (undocumented)
protected readonly children: {
readonly children: {
[key: string]: IEmbeddable<any, any> | ErrorEmbeddable;
};
// (undocumented)

View file

@ -138,7 +138,7 @@ export interface UiActionsActionDefinition<Context extends object = object> exte
// @public
export interface UiActionsPresentable<Context = unknown> {
getDisplayName(context: Context): string;
getDisplayNameTooltip(context: Context): string;
getDisplayNameTooltip?(context: Context): string;
getHref?(context: Context): Promise<string | undefined>;
getIconType(context: Context): string | undefined;
readonly grouping?: UiActionsPresentableGrouping<Context>;

View file

@ -43,7 +43,7 @@ export interface Presentable<Context = unknown> {
* Returns tooltip text which should be displayed when user hovers this object.
* Should return empty string if tooltip should not be displayed.
*/
getDisplayNameTooltip(context: Context): string;
getDisplayNameTooltip?(context: Context): string;
/**
* This method should return a link if this item can be clicked on. The link

View file

@ -46,7 +46,7 @@ export const DrilldownsWithEmbeddableExample: React.FC = () => {
);
const [showManager, setShowManager] = React.useState(false);
const [openPopup, setOpenPopup] = React.useState(false);
const viewRef = React.useRef<'create' | 'manage'>('create');
const viewRef = React.useRef<'/create' | '/manage'>('/create');
const panels: EuiContextMenuPanelDescriptor[] = [
{
@ -57,7 +57,7 @@ export const DrilldownsWithEmbeddableExample: React.FC = () => {
icon: 'plusInCircle',
onClick: () => {
setOpenPopup(false);
viewRef.current = 'create';
viewRef.current = '/create';
setShowManager((x) => !x);
},
},
@ -66,7 +66,7 @@ export const DrilldownsWithEmbeddableExample: React.FC = () => {
icon: 'list',
onClick: () => {
setOpenPopup(false);
viewRef.current = 'manage';
viewRef.current = '/manage';
setShowManager((x) => !x);
},
},
@ -122,12 +122,13 @@ export const DrilldownsWithEmbeddableExample: React.FC = () => {
{showManager && (
<EuiFlyout onClose={() => setShowManager(false)} aria-labelledby="Drilldown Manager">
<plugins.uiActionsEnhanced.FlyoutManageDrilldowns
onClose={() => setShowManager(false)}
viewMode={viewRef.current}
<plugins.uiActionsEnhanced.DrilldownManager
key={viewRef.current}
initialRoute={viewRef.current}
dynamicActionManager={managerWithEmbeddable}
triggers={[VALUE_CLICK_TRIGGER]}
placeContext={{ embeddable }}
onClose={() => setShowManager(false)}
/>
</EuiFlyout>
)}

View file

@ -32,7 +32,7 @@ export const DrilldownsWithoutEmbeddableExample: React.FC = () => {
const { plugins, managerWithoutEmbeddable } = useUiActions();
const [showManager, setShowManager] = React.useState(false);
const [openPopup, setOpenPopup] = React.useState(false);
const viewRef = React.useRef<'create' | 'manage'>('create');
const viewRef = React.useRef<'/create' | '/manage'>('/create');
const panels: EuiContextMenuPanelDescriptor[] = [
{
@ -43,7 +43,7 @@ export const DrilldownsWithoutEmbeddableExample: React.FC = () => {
icon: 'plusInCircle',
onClick: () => {
setOpenPopup(false);
viewRef.current = 'create';
viewRef.current = '/create';
setShowManager((x) => !x);
},
},
@ -52,7 +52,7 @@ export const DrilldownsWithoutEmbeddableExample: React.FC = () => {
icon: 'list',
onClick: () => {
setOpenPopup(false);
viewRef.current = 'manage';
viewRef.current = '/manage';
setShowManager((x) => !x);
},
},
@ -116,11 +116,12 @@ export const DrilldownsWithoutEmbeddableExample: React.FC = () => {
{showManager && (
<EuiFlyout onClose={() => setShowManager(false)} aria-labelledby="Drilldown Manager">
<plugins.uiActionsEnhanced.FlyoutManageDrilldowns
onClose={() => setShowManager(false)}
viewMode={viewRef.current}
<plugins.uiActionsEnhanced.DrilldownManager
key={viewRef.current}
initialRoute={viewRef.current}
dynamicActionManager={managerWithoutEmbeddable}
triggers={[SAMPLE_APP1_CLICK_TRIGGER]}
onClose={() => setShowManager(false)}
/>
</EuiFlyout>
)}

View file

@ -13,7 +13,6 @@ import { sampleApp2ClickContext, SAMPLE_APP2_CLICK_TRIGGER } from '../../trigger
export const DrilldownsWithoutEmbeddableSingleButtonExample: React.FC = () => {
const { plugins, managerWithoutEmbeddableSingleButton } = useUiActions();
const [showManager, setShowManager] = React.useState(false);
const viewRef = React.useRef<'create' | 'manage'>('create');
return (
<>
@ -50,11 +49,11 @@ export const DrilldownsWithoutEmbeddableSingleButtonExample: React.FC = () => {
{showManager && (
<EuiFlyout onClose={() => setShowManager(false)} aria-labelledby="Drilldown Manager">
<plugins.uiActionsEnhanced.FlyoutManageDrilldowns
onClose={() => setShowManager(false)}
viewMode={viewRef.current}
<plugins.uiActionsEnhanced.DrilldownManager
initialRoute={'/create'}
dynamicActionManager={managerWithoutEmbeddableSingleButton}
triggers={[SAMPLE_APP2_CLICK_TRIGGER]}
onClose={() => setShowManager(false)}
/>
</EuiFlyout>
)}

View file

@ -86,9 +86,9 @@ export class UiActionsEnhancedExamplesPlugin
const { core: coreStart, plugins: pluginsStart, self } = start();
const handle = coreStart.overlays.openFlyout(
toMountPoint(
h(pluginsStart.uiActionsEnhanced.FlyoutManageDrilldowns, {
h(pluginsStart.uiActionsEnhanced.DrilldownManager, {
onClose: () => handle.close(),
viewMode: 'create',
initialRoute: '/create',
dynamicActionManager: self.managerWithoutEmbeddableSingleButton,
triggers: [SAMPLE_APP2_CLICK_TRIGGER],
placeContext: {},
@ -111,9 +111,9 @@ export class UiActionsEnhancedExamplesPlugin
const { core: coreStart, plugins: pluginsStart, self } = start();
const handle = coreStart.overlays.openFlyout(
toMountPoint(
h(pluginsStart.uiActionsEnhanced.FlyoutManageDrilldowns, {
h(pluginsStart.uiActionsEnhanced.DrilldownManager, {
onClose: () => handle.close(),
viewMode: 'manage',
initialRoute: '/manage',
dynamicActionManager: self.managerWithoutEmbeddableSingleButton,
triggers: [SAMPLE_APP2_CLICK_TRIGGER],
placeContext: { sampleApp2ClickContext },

View file

@ -9,7 +9,11 @@ import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/data/publ
import {
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
IEmbeddable,
Container as EmbeddableContainer,
} from '../../../../../../../src/plugins/embeddable/public';
import { isEnhancedEmbeddable } from '../../../../../embeddable_enhanced/public';
import { UiActionsEnhancedDrilldownTemplate as DrilldownTemplate } from '../../../../../ui_actions_enhanced/public';
/**
* We know that VALUE_CLICK_TRIGGER and SELECT_RANGE_TRIGGER are also triggering APPLY_FILTER_TRIGGER.
@ -31,3 +35,47 @@ export function ensureNestedTriggers(triggers: string[]): string[] {
return triggers;
}
const isEmbeddableContainer = (x: unknown): x is EmbeddableContainer =>
x instanceof EmbeddableContainer;
/**
* Given a dashboard panel embeddable, it will find the parent (dashboard
* container embeddable), then iterate through all the dashboard panels and
* generate DrilldownTemplate for each existing drilldown.
*/
export const createDrilldownTemplatesFromSiblings = (
embeddable: IEmbeddable
): DrilldownTemplate[] => {
const templates: DrilldownTemplate[] = [];
const embeddableId = embeddable.id;
const container = embeddable.getRoot();
if (!container) return templates;
if (!isEmbeddableContainer(container)) return templates;
const childrenIds = (container as EmbeddableContainer).getChildIds();
for (const childId of childrenIds) {
const child = (container as EmbeddableContainer).getChild(childId);
if (child.id === embeddableId) continue;
if (!isEnhancedEmbeddable(child)) continue;
const events = child.enhancements.dynamicActions.state.get().events;
for (const event of events) {
const template: DrilldownTemplate = {
id: event.eventId,
name: event.action.name,
icon: 'dashboardApp',
description: child.getTitle() || child.id,
config: event.action.config,
factoryId: event.action.factoryId,
triggers: event.triggers,
};
templates.push(template);
}
}
return templates;
};

View file

@ -9,17 +9,17 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { Action } from '../../../../../../../../src/plugins/ui_actions/public';
import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public';
import {
isEnhancedEmbeddable,
embeddableEnhancedDrilldownGrouping,
} from '../../../../../../embeddable_enhanced/public';
import {
CONTEXT_MENU_TRIGGER,
EmbeddableContext,
} from '../../../../../../../../src/plugins/embeddable/public';
import {
isEnhancedEmbeddable,
embeddableEnhancedDrilldownGrouping,
} from '../../../../../../embeddable_enhanced/public';
import { StartDependencies } from '../../../../plugin';
import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public';
import { ensureNestedTriggers } from '../drilldown_shared';
import { ensureNestedTriggers, createDrilldownTemplatesFromSiblings } from '../drilldown_shared';
export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN';
@ -81,14 +81,18 @@ export class FlyoutCreateDrilldownAction implements Action<EmbeddableContext> {
);
}
const templates = createDrilldownTemplatesFromSiblings(embeddable);
const handle = core.overlays.openFlyout(
toMountPoint(
<plugins.uiActionsEnhanced.FlyoutManageDrilldowns
onClose={() => handle.close()}
viewMode={'create'}
<plugins.uiActionsEnhanced.DrilldownManager
closeAfterCreate
initialRoute={'/new'}
dynamicActionManager={embeddable.enhancements.dynamicActions}
triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]}
placeContext={{ embeddable }}
templates={templates}
onClose={() => handle.close()}
/>
),
{

View file

@ -24,7 +24,7 @@ import {
} from '../../../../../../embeddable_enhanced/public';
import { StartDependencies } from '../../../../plugin';
import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public';
import { ensureNestedTriggers } from '../drilldown_shared';
import { createDrilldownTemplatesFromSiblings, ensureNestedTriggers } from '../drilldown_shared';
export const OPEN_FLYOUT_EDIT_DRILLDOWN = 'OPEN_FLYOUT_EDIT_DRILLDOWN';
@ -66,14 +66,17 @@ export class FlyoutEditDrilldownAction implements Action<EmbeddableContext> {
);
}
const templates = createDrilldownTemplatesFromSiblings(embeddable);
const handle = core.overlays.openFlyout(
toMountPoint(
<plugins.uiActionsEnhanced.FlyoutManageDrilldowns
onClose={() => handle.close()}
viewMode={'manage'}
<plugins.uiActionsEnhanced.DrilldownManager
initialRoute={'/manage'}
dynamicActionManager={embeddable.enhancements.dynamicActions}
triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]}
placeContext={{ embeddable }}
templates={templates}
onClose={() => handle.close()}
/>
),
{

View file

@ -22527,10 +22527,8 @@
"xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "ドリルダウンにより、パネルと連携する新しい動作を定義できます。複数のアクションを追加し、デフォルトフィルターを無効化できます。",
"xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "非表示",
"xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "ドキュメントを表示",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownButtonLabel": "ドリルダウンを作成",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownTitle": "ドリルダウンを作成",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel": "ドリルダウンを削除",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownButtonLabel": "保存",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownTitle": "ドリルダウンを編集",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError": "不十分なライセンスレベル",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType": "ドリルダウンタイプ{type}が存在しません",
@ -22545,15 +22543,6 @@
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle": "{n}個のドリルダウンが削除されました",
"xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.BackButtonLabel": "戻る",
"xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.CloseButtonLabel": "閉じる",
"xpack.uiActionsEnhanced.drilldowns.components.FlyoutListManageDrilldowns.manageDrilldownsTitle": "ドリルダウンを管理",
"xpack.uiActionsEnhanced.drilldowns.components.FormCreateDrilldown.drilldownAction": "アクション",
"xpack.uiActionsEnhanced.drilldowns.components.FormCreateDrilldown.nameOfDrilldown": "名前",
"xpack.uiActionsEnhanced.drilldowns.components.FormCreateDrilldown.untitledDrilldown": "無題のドリルダウン",
"xpack.uiActionsEnhanced.drilldowns.components.FormDrilldownWizard.getMoreActionsLinkLabel": "さらにアクションを表示",
"xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.createDrilldownButtonLabel": "新規作成",
"xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.deleteDrilldownsButtonLabel": "削除 ({count}) ",
"xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.editDrilldownButtonLabel": "編集",
"xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.selectThisDrilldownCheckboxLabel": "このドリルダウンを選択",
"xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions": "その他のオプション",
"xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle": "変数を追加",
"xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription": "有効な場合、URLはパーセントエンコーディングを使用してエスケープされます",

View file

@ -22885,10 +22885,8 @@
"xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "向下钻取允许您定义与面板交互的新行为。您可以添加多个操作并覆盖默认筛选。",
"xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "隐藏",
"xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "查看文档",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownButtonLabel": "创建向下钻取",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownTitle": "创建向下钻取",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel": "删除向下钻取",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownButtonLabel": "保存",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownTitle": "编辑向下钻取",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError": "许可证级别不够",
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType": "向下钻取类型 {type} 不存在",
@ -22903,15 +22901,6 @@
"xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle": "{n} 个向下钻取已删除",
"xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.BackButtonLabel": "返回",
"xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.CloseButtonLabel": "关闭",
"xpack.uiActionsEnhanced.drilldowns.components.FlyoutListManageDrilldowns.manageDrilldownsTitle": "管理向下钻取",
"xpack.uiActionsEnhanced.drilldowns.components.FormCreateDrilldown.drilldownAction": "操作",
"xpack.uiActionsEnhanced.drilldowns.components.FormCreateDrilldown.nameOfDrilldown": "名称",
"xpack.uiActionsEnhanced.drilldowns.components.FormCreateDrilldown.untitledDrilldown": "未命名向下钻取",
"xpack.uiActionsEnhanced.drilldowns.components.FormDrilldownWizard.getMoreActionsLinkLabel": "获取更多的操作",
"xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.createDrilldownButtonLabel": "新建",
"xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.deleteDrilldownsButtonLabel": "删除 ({count})",
"xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.editDrilldownButtonLabel": "编辑",
"xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.selectThisDrilldownCheckboxLabel": "选择此向下钻取",
"xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions": "其他选项",
"xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle": "添加变量",
"xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription": "如果启用,将使用百分比编码转义 URL",

View file

@ -0,0 +1,52 @@
/*
* 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 { ActionFactory, BaseActionFactoryContext } from '../../dynamic_actions';
import { PresentablePicker, Item } from '../presentable_picker';
export interface ActionFactoryPickerProps {
actionFactories: ActionFactory[];
context: unknown;
onSelect: (actionFactory: ActionFactory) => void;
}
export const ActionFactoryPicker: React.FC<ActionFactoryPickerProps> = ({
actionFactories,
context,
onSelect,
}) => {
const items = React.useMemo(() => {
return actionFactories.map((actionFactory) => {
const item: Item = {
id: actionFactory.id,
order: actionFactory.order,
getDisplayName: (ctx: unknown) =>
actionFactory.getDisplayName(ctx as BaseActionFactoryContext),
getIconType: (ctx: unknown) => actionFactory.getIconType(ctx as BaseActionFactoryContext),
getDisplayNameTooltip: () => '',
isCompatible: (ctx: unknown) => actionFactory.isCompatible(ctx as BaseActionFactoryContext),
MenuItem: actionFactory.MenuItem,
isBeta: actionFactory.isBeta,
isLicenseCompatible: actionFactory.isCompatibleLicense(),
};
return item;
});
}, [actionFactories]);
const handleSelect = React.useCallback(
(id: string) => {
if (!onSelect) return;
const actionFactory = actionFactories.find((af) => af.id === id);
if (!actionFactory) return;
onSelect(actionFactory);
},
[onSelect, actionFactories]
);
return <PresentablePicker items={items} context={context} onSelect={handleSelect} />;
};

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export * from './flyout_drilldown_wizard';
export * from './action_factory_picker';

View file

@ -99,23 +99,29 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
triggerPickerDocsLink,
}) => {
// auto pick action factory if there is only 1 available
if (
!currentActionFactory &&
actionFactories.length === 1 &&
actionFactories[0].isCompatibleLicense()
) {
onActionFactoryChange(actionFactories[0]);
}
React.useEffect(() => {
if (
!currentActionFactory &&
actionFactories.length === 1 &&
actionFactories[0].isCompatibleLicense()
) {
onActionFactoryChange(actionFactories[0]);
}
}, [currentActionFactory, actionFactories, actionFactories.length, onActionFactoryChange]);
// auto pick selected trigger if none is picked
if (currentActionFactory && !((context.triggers?.length ?? 0) > 0)) {
const actionTriggers = getTriggersForActionFactory(currentActionFactory, triggers);
if (actionTriggers.length > 0) {
onSelectedTriggersChange([actionTriggers[0]]);
React.useEffect(() => {
if (currentActionFactory && !((context.triggers?.length ?? 0) > 0)) {
const actionTriggers = getTriggersForActionFactory(currentActionFactory, triggers);
if (actionTriggers.length > 0) {
onSelectedTriggersChange([actionTriggers[0]]);
}
}
}
}, [currentActionFactory, triggers, context.triggers?.length, onSelectedTriggersChange]);
if (currentActionFactory) {
if (!config) return null;
if (currentActionFactory && config) {
const allTriggers = getTriggersForActionFactory(currentActionFactory, triggers);
return (
<SelectedActionFactory
@ -141,9 +147,7 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
<ActionFactorySelector
context={context}
actionFactories={actionFactories}
onActionFactorySelected={(actionFactory) => {
onActionFactoryChange(actionFactory);
}}
onActionFactorySelected={onActionFactoryChange}
/>
);
};

View file

@ -0,0 +1,29 @@
/*
* 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';
export const txtBetaActionFactoryLabel = i18n.translate(
'xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel',
{
defaultMessage: `Beta`,
}
);
export const txtBetaActionFactoryTooltip = i18n.translate(
'xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip',
{
defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting any bugs or providing other feedback.`,
}
);
export const txtInsufficientLicenseLevel = i18n.translate(
'xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip',
{
defaultMessage: 'Insufficient license level',
}
);

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export * from './flyout_list_manage_drilldowns';
export * from './presentable_picker';

View file

@ -0,0 +1,152 @@
/*
* 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 { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { PresentablePicker } from './presentable_picker';
storiesOf('components/PresentablePicker', module)
.add('One item', () => (
<PresentablePicker
items={[
{
id: 'URL',
getDisplayName: () => 'Go to URL',
getIconType: () => 'link',
getDisplayNameTooltip: () => '',
order: 10,
isCompatible: async (context?: object) => true,
},
]}
context={{}}
onSelect={action('onSelect')}
/>
))
.add('Items are sorted', () => (
<PresentablePicker
items={[
{
id: 'item2',
getDisplayName: () => 'Item 2',
getIconType: () => 'link',
getDisplayNameTooltip: () => '',
order: 1,
isCompatible: async (context?: object) => true,
},
{
id: 'item1',
getDisplayName: () => 'Item 1',
getIconType: () => 'link',
getDisplayNameTooltip: () => '',
order: 2,
isCompatible: async (context?: object) => true,
},
]}
context={{}}
onSelect={action('onSelect')}
/>
))
.add('Items are sorted - 2', () => (
<PresentablePicker
items={[
{
id: 'item1',
getDisplayName: () => 'Item 1',
getIconType: () => 'link',
getDisplayNameTooltip: () => '',
order: 2,
isCompatible: async (context?: object) => true,
},
{
id: 'item2',
getDisplayName: () => 'Item 2',
getIconType: () => 'link',
getDisplayNameTooltip: () => '',
order: 1,
isCompatible: async (context?: object) => true,
},
]}
context={{}}
onSelect={action('onSelect')}
/>
))
.add('Two items', () => (
<PresentablePicker
items={[
{
id: 'URL',
getDisplayName: () => 'Go to URL',
getIconType: () => 'link',
getDisplayNameTooltip: () => '',
order: 2,
isCompatible: async (context?: object) => true,
},
{
id: 'DASHBOARD',
getDisplayName: () => 'Go to Dashboard',
getIconType: () => 'dashboardApp',
getDisplayNameTooltip: () => '',
order: 1,
isCompatible: async (context?: object) => true,
},
]}
context={{}}
onSelect={action('onSelect')}
/>
))
.add('Beta badge', () => (
<PresentablePicker
items={[
{
id: 'URL',
getDisplayName: () => 'Go to URL',
getIconType: () => 'link',
getDisplayNameTooltip: () => '',
order: 2,
isCompatible: async (context?: object) => true,
isBeta: true,
},
{
id: 'DASHBOARD',
getDisplayName: () => 'Go to Dashboard',
getIconType: () => 'dashboardApp',
getDisplayNameTooltip: () => '',
order: 1,
isCompatible: async (context?: object) => true,
},
]}
context={{}}
onSelect={action('onSelect')}
/>
))
.add('Incompatible license', () => (
<PresentablePicker
items={[
{
id: 'URL',
getDisplayName: () => 'Go to URL',
getIconType: () => 'link',
getDisplayNameTooltip: () => '',
order: 2,
isCompatible: async (context?: object) => true,
isBeta: true,
isLicenseCompatible: false,
},
{
id: 'DASHBOARD',
getDisplayName: () => 'Go to Dashboard',
getIconType: () => 'dashboardApp',
getDisplayNameTooltip: () => '',
order: 1,
isCompatible: async (context?: object) => true,
},
]}
context={{}}
onSelect={action('onSelect')}
/>
));

View file

@ -0,0 +1,57 @@
/*
* 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 { EuiFlexGroup } from '@elastic/eui';
import { PresentablePickerItem, Item } from './presentable_picker_item';
export { Item } from './presentable_picker_item';
export interface PresentablePickerProps {
items: Item[];
context: unknown;
onSelect: (itemId: string) => void;
}
export const TEST_SUBJ_ACTION_FACTORY_ITEM = 'actionFactoryItem';
// The below style is applied to fix Firefox rendering bug.
// See: https://github.com/elastic/kibana/pull/61219/#pullrequestreview-402903330
const firefoxBugFix = {
willChange: 'opacity',
};
const sort = (f1: Item, f2: Item): number => f2.order - f1.order;
export const PresentablePicker: React.FC<PresentablePickerProps> = ({
items,
context,
onSelect,
}) => {
/**
* Make sure items with incompatible license are at the end.
*/
const itemsSorted = React.useMemo(() => {
const compatible = items.filter((f) => f.isLicenseCompatible ?? true);
const incompatible = items.filter((f) => !(f.isLicenseCompatible ?? true));
return [...compatible.sort(sort), ...incompatible.sort(sort)];
}, [items]);
if (items.length === 0) {
// This is not user facing, as it would be impossible to get into this state
// just leaving for dev purposes for troubleshooting.
return <div>No action factories to pick from.</div>;
}
return (
<EuiFlexGroup gutterSize="m" responsive={false} wrap={true} style={firefoxBugFix}>
{itemsSorted.map((item) => (
<PresentablePickerItem key={item.id} item={item} context={context} onSelect={onSelect} />
))}
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,63 @@
/*
* 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 { EuiFlexItem, EuiIcon, EuiKeyPadMenuItem, EuiToolTip } from '@elastic/eui';
import {
txtBetaActionFactoryLabel,
txtBetaActionFactoryTooltip,
txtInsufficientLicenseLevel,
} from './i18n';
import { UiActionsPresentable as Presentable } from '../../../../../../src/plugins/ui_actions/public';
import './styles.scss';
export interface Item extends Presentable {
isLicenseCompatible?: boolean;
isBeta?: boolean;
}
export interface PresentablePickerItemProps {
item: Item;
context: unknown;
onSelect: (itemId: string) => void;
}
export const TEST_SUBJ_PRESENTABLE_ITEM = 'actionFactoryItem';
export const PresentablePickerItem: React.FC<PresentablePickerItemProps> = ({
item,
context,
onSelect,
}) => {
const isLicenseCompatible = item.isLicenseCompatible ?? true;
const showTooltip = !isLicenseCompatible;
let content = (
<EuiKeyPadMenuItem
className="auaPresentablePicker__item"
label={item.getDisplayName(context)}
data-test-subj={`${TEST_SUBJ_PRESENTABLE_ITEM}-${item.id}`}
onClick={() => onSelect(item.id)}
disabled={!isLicenseCompatible}
betaBadgeLabel={item.isBeta ? txtBetaActionFactoryLabel : undefined}
betaBadgeTooltipContent={item.isBeta ? txtBetaActionFactoryTooltip : undefined}
>
{item.getIconType(context) && <EuiIcon type={item.getIconType(context)!} size="m" />}
</EuiKeyPadMenuItem>
);
if (showTooltip) {
content = <EuiToolTip content={txtInsufficientLicenseLevel}>{content}</EuiToolTip>;
}
return (
<EuiFlexItem grow={false} key={item.id}>
{content}
</EuiFlexItem>
);
};

View file

@ -0,0 +1,5 @@
.auaPresentablePicker__item {
.euiKeyPadMenuItem__label {
height: #{$euiSizeXL};
}
}

View file

@ -1,49 +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 * as React from 'react';
import { EuiFlyout } from '@elastic/eui';
import { storiesOf } from '@storybook/react';
import { StubBrowserStorage } from '@kbn/test/jest';
import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns';
import { mockActionFactories } from '../../../components/action_wizard/test_data';
import { Storage } from '../../../../../../../src/plugins/kibana_utils/public';
import { mockDynamicActionManager } from './test_data';
const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({
actionFactories: mockActionFactories,
storage: new Storage(new StubBrowserStorage()),
toastService: {
addError: (...args: any[]) => {
alert(JSON.stringify(args));
},
addSuccess: (...args: any[]) => {
alert(JSON.stringify(args));
},
} as any,
getTrigger: (triggerId) => ({
id: triggerId,
}),
});
storiesOf('components/FlyoutManageDrilldowns', module)
.add('default (3 triggers)', () => (
<EuiFlyout onClose={() => {}}>
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
triggers={['VALUE_CLICK_TRIGGER', 'SELECT_RANGE_TRIGGER', 'FILTER_TRIGGER']}
/>
</EuiFlyout>
))
.add('Only filter is supported', () => (
<EuiFlyout onClose={() => {}}>
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
triggers={['FILTER_TRIGGER']}
/>
</EuiFlyout>
));

View file

@ -1,324 +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 from 'react';
import { fireEvent, render, waitFor, cleanup } from '@testing-library/react';
import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns';
import {
mockGetTriggerInfo,
mockSupportedTriggers,
mockActionFactories,
} from '../../../components/action_wizard/test_data';
import { StubBrowserStorage } from '@kbn/test/jest';
import { Storage } from '../../../../../../../src/plugins/kibana_utils/public';
import { mockDynamicActionManager } from './test_data';
import { TEST_SUBJ_DRILLDOWN_ITEM } from '../list_manage_drilldowns';
import { WELCOME_MESSAGE_TEST_SUBJ } from '../drilldown_hello_bar';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { NotificationsStart } from 'kibana/public';
import { toastDrilldownsCRUDError } from '../../hooks/i18n';
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
const storage = new Storage(new StubBrowserStorage());
const toasts = coreMock.createStart().notifications.toasts;
const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({
actionFactories: mockActionFactories,
storage: new Storage(new StubBrowserStorage()),
toastService: toasts,
getTrigger: mockGetTriggerInfo,
});
beforeEach(() => {
storage.clear();
mockDynamicActionManager.state.set({ ...mockDynamicActionManager.state.get(), events: [] });
(toasts as jest.Mocked<NotificationsStart['toasts']>).addSuccess.mockClear();
(toasts as jest.Mocked<NotificationsStart['toasts']>).addError.mockClear();
});
test('Allows to manage drilldowns', async () => {
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible());
// no drilldowns in the list
expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0);
fireEvent.click(screen.getByText(/Create new/i));
let [createHeading] = screen.getAllByText(/Create Drilldown/i);
let createButton = screen.getByRole('button', { name: /Create Drilldown/i });
expect(createHeading).toBeVisible();
expect(screen.getByLabelText(/Back/i)).toBeVisible();
expect(createButton).toBeDisabled();
// input drilldown name
const name = 'Test name';
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: name },
});
// select URL one
fireEvent.click(screen.getByText(/Go to URL/i));
// Input url
const URL = 'https://elastic.co';
fireEvent.change(screen.getByLabelText(/url/i), {
target: { value: URL },
});
[createHeading] = screen.getAllByText(/Create Drilldown/i);
createButton = screen.getByRole('button', { name: /Create Drilldown/i });
expect(createButton).toBeEnabled();
fireEvent.click(createButton);
expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible();
await waitFor(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(1));
expect(screen.getByText(name)).toBeVisible();
const editButton = screen.getByText(/edit/i);
fireEvent.click(editButton);
expect(screen.getByText(/Edit Drilldown/i)).toBeVisible();
// check that wizard is prefilled with current drilldown values
expect(screen.getByLabelText(/name/i)).toHaveValue(name);
expect(screen.getByLabelText(/url/i)).toHaveValue(URL);
// input new drilldown name
const newName = 'New drilldown name';
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: newName },
});
fireEvent.click(screen.getByText(/save/i));
expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible();
await waitFor(() => screen.getByText(newName));
// delete drilldown from edit view
fireEvent.click(screen.getByText(/edit/i));
fireEvent.click(screen.getByText(/delete/i));
expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible();
await waitFor(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0));
});
test('Can delete multiple drilldowns', async () => {
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible());
const createDrilldown = async () => {
const oldCount = screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM).length;
fireEvent.click(screen.getByText(/Create new/i));
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: 'test' },
});
fireEvent.click(screen.getByText(/Go to URL/i));
fireEvent.change(screen.getByLabelText(/url/i), {
target: { value: 'https://elastic.co' },
});
fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]);
await waitFor(() =>
expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(oldCount + 1)
);
};
await createDrilldown();
await createDrilldown();
await createDrilldown();
const checkboxes = screen.getAllByLabelText(/Select this drilldown/i);
expect(checkboxes).toHaveLength(3);
checkboxes.forEach((checkbox) => fireEvent.click(checkbox));
expect(screen.queryByText(/Create/i)).not.toBeInTheDocument();
fireEvent.click(screen.getByText(/Delete \(3\)/i));
await waitFor(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0));
});
test('Create only mode', async () => {
const onClose = jest.fn();
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
viewMode={'create'}
onClose={onClose}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0));
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: 'test' },
});
fireEvent.click(screen.getByText(/Go to URL/i));
fireEvent.change(screen.getByLabelText(/url/i), {
target: { value: 'https://elastic.co' },
});
fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]);
await waitFor(() => expect(toasts.addSuccess).toBeCalled());
expect(onClose).toBeCalled();
expect(await mockDynamicActionManager.state.get().events.length).toBe(1);
});
test('After switching between action factories state is restored', async () => {
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
viewMode={'create'}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0));
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: 'test' },
});
fireEvent.click(screen.getByText(/Go to URL/i));
fireEvent.change(screen.getByLabelText(/url/i), {
target: { value: 'https://elastic.co' },
});
// change to dashboard
fireEvent.click(screen.getByText(/change/i));
fireEvent.click(screen.getByText(/Go to Dashboard/i));
// change back to url
fireEvent.click(screen.getByText(/change/i));
fireEvent.click(screen.getByText(/Go to URL/i));
expect(screen.getByLabelText(/url/i)).toHaveValue('https://elastic.co');
expect(screen.getByLabelText(/name/i)).toHaveValue('test');
fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]);
await waitFor(() => expect(toasts.addSuccess).toBeCalled());
expect(await (mockDynamicActionManager.state.get().events[0].action.config as any).url).toBe(
'https://elastic.co'
);
});
test.todo("Error when can't fetch drilldown list");
test("Error when can't save drilldown changes", async () => {
const error = new Error('Oops');
jest.spyOn(mockDynamicActionManager, 'createEvent').mockImplementationOnce(async () => {
throw error;
});
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible());
fireEvent.click(screen.getByText(/Create new/i));
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: 'test' },
});
fireEvent.click(screen.getByText(/Go to URL/i));
fireEvent.change(screen.getByLabelText(/url/i), {
target: { value: 'https://elastic.co' },
});
fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]);
await waitFor(() =>
expect(toasts.addError).toBeCalledWith(error, { title: toastDrilldownsCRUDError })
);
});
test('Should show drilldown welcome message. Should be able to dismiss it', async () => {
let screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible());
expect(screen.getByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeVisible();
fireEvent.click(screen.getByText(/hide/i));
expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull();
cleanup();
screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
triggers={mockSupportedTriggers}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible());
expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull();
});
test('Drilldown type is not shown if no supported trigger', async () => {
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
triggers={['VALUE_CLICK_TRIGGER']}
viewMode={'create'}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0));
expect(screen.queryByText(/Go to Dashboard/i)).not.toBeInTheDocument(); // dashboard action is not visible, because APPLY_FILTER_TRIGGER not supported
expect(screen.getByTestId('selectedActionFactory-Url')).toBeInTheDocument();
});
test('Can pick a trigger', async () => {
const screen = render(
<FlyoutManageDrilldowns
dynamicActionManager={mockDynamicActionManager}
triggers={mockSupportedTriggers}
viewMode={'create'}
/>
);
// wait for initial render. It is async because resolving compatible action factories is async
await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0));
// input drilldown name
const name = 'Test name';
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: name },
});
// select URL one
fireEvent.click(screen.getByText(/Go to URL/i));
// Input url
const URL = 'https://elastic.co';
fireEvent.change(screen.getByLabelText(/url/i), {
target: { value: URL },
});
fireEvent.click(screen.getByTestId('triggerPicker-SELECT_RANGE_TRIGGER').querySelector('input')!);
const [, createButton] = screen.getAllByText(/Create Drilldown/i);
expect(createButton).toBeEnabled();
fireEvent.click(createButton);
await waitFor(() => expect(toasts.addSuccess).toBeCalled());
expect(mockDynamicActionManager.state.get().events[0].triggers).toEqual(['SELECT_RANGE_TRIGGER']);
});

View file

@ -1,245 +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, { useState, useMemo } from 'react';
import { ToastsStart } from 'kibana/public';
import { intersection } from 'lodash';
import { DrilldownWizardConfig, FlyoutDrilldownWizard } from '../flyout_drilldown_wizard';
import { FlyoutListManageDrilldowns } from '../flyout_list_manage_drilldowns';
import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public';
import { Trigger } from '../../../../../../../src/plugins/ui_actions/public';
import { DrilldownListItem } from '../list_manage_drilldowns';
import { insufficientLicenseLevel, invalidDrilldownType } from './i18n';
import {
ActionFactory,
BaseActionConfig,
BaseActionFactoryContext,
DynamicActionManager,
SerializedEvent,
} from '../../../dynamic_actions';
import { useWelcomeMessage } from '../../hooks/use_welcome_message';
import { useCompatibleActionFactoriesForCurrentContext } from '../../hooks/use_compatible_action_factories_for_current_context';
import { useDrilldownsStateManager } from '../../hooks/use_drilldown_state_manager';
import { ActionFactoryPlaceContext } from '../types';
interface ConnectedFlyoutManageDrilldownsProps<
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
> {
dynamicActionManager: DynamicActionManager;
viewMode?: 'create' | 'manage';
onClose?: () => void;
/**
* List of possible triggers in current context
*/
triggers: string[];
/**
* Extra action factory context passed into action factories CollectConfig, getIconType, getDisplayName and etc...
*/
placeContext?: ActionFactoryPlaceContext<ActionFactoryContext>;
}
/**
* Represent current state (route) of FlyoutManageDrilldowns
*/
enum Routes {
Manage = 'manage',
Create = 'create',
Edit = 'edit',
}
export function createFlyoutManageDrilldowns({
actionFactories: allActionFactories,
storage,
toastService,
docsLink,
triggerPickerDocsLink,
getTrigger,
}: {
actionFactories: ActionFactory[];
getTrigger: (triggerId: string) => Trigger;
storage: IStorageWrapper;
toastService: ToastsStart;
docsLink?: string;
triggerPickerDocsLink?: string;
}): React.FC<ConnectedFlyoutManageDrilldownsProps> {
const allActionFactoriesById = allActionFactories.reduce((acc, next) => {
acc[next.id] = next;
return acc;
}, {} as Record<string, ActionFactory>);
return (props: ConnectedFlyoutManageDrilldownsProps) => {
const isCreateOnly = props.viewMode === 'create';
const factoryContext: BaseActionFactoryContext = useMemo(
() => ({ ...props.placeContext, triggers: props.triggers }),
[props.placeContext, props.triggers]
);
const actionFactories = useCompatibleActionFactoriesForCurrentContext(
allActionFactories,
factoryContext
);
const [route, setRoute] = useState<Routes>(
() => (isCreateOnly ? Routes.Create : Routes.Manage) // initial state is different depending on `viewMode`
);
const [currentEditId, setCurrentEditId] = useState<string | null>(null);
const [shouldShowWelcomeMessage, onHideWelcomeMessage] = useWelcomeMessage(storage);
const {
drilldowns,
createDrilldown,
editDrilldown,
deleteDrilldown,
} = useDrilldownsStateManager(props.dynamicActionManager, toastService);
/**
* isCompatible promise is not yet resolved.
* Skip rendering until it is resolved
*/
if (!actionFactories) return null;
/**
* Drilldowns are not fetched yet or error happened during fetching
* In case of error user is notified with toast
*/
if (!drilldowns) return null;
/**
* Needed for edit mode to prefill wizard fields with data from current edited drilldown
*/
function resolveInitialDrilldownWizardConfig(): DrilldownWizardConfig | undefined {
if (route !== Routes.Edit) return undefined;
if (!currentEditId) return undefined;
const drilldownToEdit = drilldowns?.find((d) => d.eventId === currentEditId);
if (!drilldownToEdit) return undefined;
return {
actionFactory: allActionFactoriesById[drilldownToEdit.action.factoryId],
actionConfig: drilldownToEdit.action.config as BaseActionConfig,
name: drilldownToEdit.action.name,
selectedTriggers: (drilldownToEdit.triggers ?? []) as string[],
};
}
/**
* Maps drilldown to list item view model
*/
function mapToDrilldownToDrilldownListItem(drilldown: SerializedEvent): DrilldownListItem {
const actionFactory = allActionFactoriesById[drilldown.action.factoryId];
const drilldownFactoryContext: BaseActionFactoryContext = {
...props.placeContext,
triggers: drilldown.triggers as string[],
};
return {
id: drilldown.eventId,
drilldownName: drilldown.action.name,
actionName:
actionFactory?.getDisplayName(drilldownFactoryContext) ?? drilldown.action.factoryId,
icon: actionFactory?.getIconType(drilldownFactoryContext),
error: !actionFactory
? invalidDrilldownType(drilldown.action.factoryId) // this shouldn't happen for the end user, but useful during development
: !actionFactory.isCompatibleLicense()
? insufficientLicenseLevel
: undefined,
triggers: drilldown.triggers.map((trigger) => getTrigger(trigger as string)),
};
}
switch (route) {
case Routes.Create:
case Routes.Edit:
return (
<FlyoutDrilldownWizard
docsLink={docsLink}
triggerPickerDocsLink={triggerPickerDocsLink}
showWelcomeMessage={shouldShowWelcomeMessage}
onWelcomeHideClick={onHideWelcomeMessage}
drilldownActionFactories={actionFactories}
onClose={props.onClose}
mode={route === Routes.Create ? 'create' : 'edit'}
onBack={isCreateOnly ? undefined : () => setRoute(Routes.Manage)}
onSubmit={({ actionConfig, actionFactory, name, selectedTriggers }) => {
if (route === Routes.Create) {
createDrilldown(
{
name,
config: actionConfig,
factoryId: actionFactory.id,
},
selectedTriggers
);
} else {
editDrilldown(
currentEditId!,
{
name,
config: actionConfig,
factoryId: actionFactory.id,
},
selectedTriggers
);
}
if (isCreateOnly) {
if (props.onClose) {
props.onClose();
}
} else {
setRoute(Routes.Manage);
}
setCurrentEditId(null);
}}
onDelete={() => {
deleteDrilldown(currentEditId!);
setRoute(Routes.Manage);
setCurrentEditId(null);
}}
actionFactoryPlaceContext={props.placeContext}
initialDrilldownWizardConfig={resolveInitialDrilldownWizardConfig()}
supportedTriggers={props.triggers}
getTrigger={getTrigger}
/>
);
case Routes.Manage:
default:
// show trigger column in case if there is more then 1 possible trigger in current context
const showTriggerColumn =
intersection(
props.triggers,
actionFactories
.map((factory) => factory.supportedTriggers())
.reduce((res, next) => res.concat(next), [])
).length > 1;
return (
<FlyoutListManageDrilldowns
docsLink={docsLink}
showWelcomeMessage={shouldShowWelcomeMessage}
onWelcomeHideClick={onHideWelcomeMessage}
drilldowns={drilldowns.map(mapToDrilldownToDrilldownListItem)}
onDelete={(ids) => {
setCurrentEditId(null);
deleteDrilldown(ids);
}}
onEdit={(id) => {
setCurrentEditId(id);
setRoute(Routes.Edit);
}}
onCreate={() => {
setCurrentEditId(null);
setRoute(Routes.Create);
}}
onClose={props.onClose}
showTriggerColumn={showTriggerColumn}
/>
);
}
};
}

View file

@ -1,28 +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 { i18n } from '@kbn/i18n';
export const insufficientLicenseLevel = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError',
{
defaultMessage: 'Insufficient license level',
description:
'User created drilldown with higher license type, but then downgraded the license. This error is shown in the list near created drilldown',
}
);
export const invalidDrilldownType = (type: string) =>
i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType',
{
defaultMessage: "Drilldown type {type} doesn't exist",
values: {
type,
},
}
);

View file

@ -1,87 +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 uuid from 'uuid';
import type { PublicMethodsOf } from '@kbn/utility-types';
import {
UiActionsEnhancedDynamicActionManager as DynamicActionManager,
UiActionsEnhancedDynamicActionManagerState as DynamicActionManagerState,
UiActionsEnhancedSerializedAction,
} from '../../../index';
import { createStateContainer } from '../../../../../../../src/plugins/kibana_utils/common';
class MockDynamicActionManager implements PublicMethodsOf<DynamicActionManager> {
public readonly state = createStateContainer<DynamicActionManagerState>({
isFetchingEvents: false,
fetchCount: 0,
events: [],
});
async count() {
return this.state.get().events.length;
}
async list() {
return this.state.get().events;
}
async createEvent(action: UiActionsEnhancedSerializedAction<any>, triggers: string[]) {
const event = {
action,
triggers,
eventId: uuid(),
};
const state = this.state.get();
this.state.set({
...state,
events: [...state.events, event],
});
}
async deleteEvents(eventIds: string[]) {
const state = this.state.get();
let events = state.events;
eventIds.forEach((id) => {
events = events.filter((e) => e.eventId !== id);
});
this.state.set({
...state,
events,
});
}
async updateEvent(
eventId: string,
action: UiActionsEnhancedSerializedAction,
triggers: string[]
) {
const state = this.state.get();
const events = state.events;
const idx = events.findIndex((e) => e.eventId === eventId);
const event = {
eventId,
action,
triggers,
};
this.state.set({
...state,
events: [...events.slice(0, idx), event, ...events.slice(idx + 1)],
});
}
async deleteEvent() {
throw new Error('not implemented');
}
async start() {}
async stop() {}
}
export const mockDynamicActionManager = (new MockDynamicActionManager() as unknown) as DynamicActionManager;

View file

@ -1,69 +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 * as React from 'react';
import { EuiFlyout } from '@elastic/eui';
import { storiesOf } from '@storybook/react';
import { FlyoutDrilldownWizard } from './index';
import { mockActionFactories } from '../../../components/action_wizard/test_data';
import { Trigger } from '../../../../../../../src/plugins/ui_actions/public';
const otherProps = {
supportedTriggers: ['VALUE_CLICK_TRIGGER', 'SELECT_RANGE_TRIGGER', 'FILTER_TRIGGER'] as string[],
onClose: () => {},
getTrigger: (id: string) => ({ id } as Trigger),
};
storiesOf('components/FlyoutDrilldownWizard', module)
.add('default', () => {
return <FlyoutDrilldownWizard drilldownActionFactories={mockActionFactories} {...otherProps} />;
})
.add('open in flyout - create', () => {
return (
<EuiFlyout onClose={() => {}}>
<FlyoutDrilldownWizard drilldownActionFactories={mockActionFactories} {...otherProps} />
</EuiFlyout>
);
})
.add('open in flyout - edit', () => {
return (
<EuiFlyout onClose={() => {}}>
<FlyoutDrilldownWizard
drilldownActionFactories={mockActionFactories}
initialDrilldownWizardConfig={{
name: 'My fancy drilldown',
actionFactory: mockActionFactories[1],
actionConfig: {
url: 'https://elastic.co',
openInNewTab: true,
},
}}
mode={'edit'}
{...otherProps}
/>
</EuiFlyout>
);
})
.add('open in flyout - edit, just 1 action type', () => {
return (
<EuiFlyout onClose={() => {}}>
<FlyoutDrilldownWizard
drilldownActionFactories={[mockActionFactories[1]]}
initialDrilldownWizardConfig={{
name: 'My fancy drilldown',
actionFactory: mockActionFactories[1],
actionConfig: {
url: 'https://elastic.co',
openInNewTab: true,
},
}}
mode={'edit'}
{...otherProps}
/>
</EuiFlyout>
);
});

View file

@ -1,248 +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, { useMemo, useState } from 'react';
import { EuiButton, EuiSpacer } from '@elastic/eui';
import { FormDrilldownWizard } from '../form_drilldown_wizard';
import { FlyoutFrame } from '../flyout_frame';
import {
txtCreateDrilldownButtonLabel,
txtCreateDrilldownTitle,
txtDeleteDrilldownButtonLabel,
txtEditDrilldownButtonLabel,
txtEditDrilldownTitle,
} from './i18n';
import { DrilldownHelloBar } from '../drilldown_hello_bar';
import {
ActionFactory,
BaseActionConfig,
BaseActionFactoryContext,
} from '../../../dynamic_actions';
import { Trigger } from '../../../../../../../src/plugins/ui_actions/public';
import { ActionFactoryPlaceContext } from '../types';
export interface DrilldownWizardConfig<ActionConfig extends BaseActionConfig = BaseActionConfig> {
name: string;
actionFactory?: ActionFactory;
actionConfig?: ActionConfig;
selectedTriggers?: string[];
}
export interface FlyoutDrilldownWizardProps<
CurrentActionConfig extends BaseActionConfig = BaseActionConfig,
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
> {
drilldownActionFactories: ActionFactory[];
onSubmit?: (drilldownWizardConfig: Required<DrilldownWizardConfig>) => void;
onDelete?: () => void;
onClose?: () => void;
onBack?: () => void;
mode?: 'create' | 'edit';
initialDrilldownWizardConfig?: DrilldownWizardConfig<CurrentActionConfig>;
showWelcomeMessage?: boolean;
onWelcomeHideClick?: () => void;
actionFactoryPlaceContext?: ActionFactoryPlaceContext<ActionFactoryContext>;
/**
* General overview of drilldowns
*/
docsLink?: string;
/**
* Link that explains different triggers
*/
triggerPickerDocsLink?: string;
getTrigger: (triggerId: string) => Trigger;
/**
* List of possible triggers in current context
*/
supportedTriggers: string[];
}
function useWizardConfigState(
actionFactoryContext: BaseActionFactoryContext,
initialDrilldownWizardConfig?: DrilldownWizardConfig
): [
DrilldownWizardConfig,
{
setName: (name: string) => void;
setActionConfig: (actionConfig: BaseActionConfig) => void;
setActionFactory: (actionFactory?: ActionFactory) => void;
setSelectedTriggers: (triggers?: string[]) => void;
}
] {
const [wizardConfig, setWizardConfig] = useState<DrilldownWizardConfig>(
() =>
initialDrilldownWizardConfig ?? {
name: '',
}
);
const [actionConfigCache, setActionConfigCache] = useState<Record<string, object>>(
initialDrilldownWizardConfig?.actionFactory
? {
[initialDrilldownWizardConfig.actionFactory
.id]: initialDrilldownWizardConfig.actionConfig!,
}
: {}
);
return [
wizardConfig,
{
setName: (name: string) => {
setWizardConfig({
...wizardConfig,
name,
});
},
setActionConfig: (actionConfig: BaseActionConfig) => {
setWizardConfig({
...wizardConfig,
actionConfig,
});
},
setActionFactory: (actionFactory?: ActionFactory) => {
if (actionFactory) {
const actionConfig = (actionConfigCache[actionFactory.id] ??
actionFactory.createConfig(actionFactoryContext)) as BaseActionConfig;
setWizardConfig({
...wizardConfig,
actionFactory,
actionConfig,
selectedTriggers: [],
});
} else {
if (wizardConfig.actionFactory?.id) {
setActionConfigCache({
...actionConfigCache,
[wizardConfig.actionFactory.id]: wizardConfig.actionConfig!,
});
}
setWizardConfig({
...wizardConfig,
actionFactory: undefined,
actionConfig: undefined,
});
}
},
setSelectedTriggers: (selectedTriggers: string[] = []) => {
setWizardConfig({
...wizardConfig,
selectedTriggers,
});
},
},
];
}
export function FlyoutDrilldownWizard<
CurrentActionConfig extends BaseActionConfig = BaseActionConfig
>({
onClose,
onBack,
onSubmit = () => {},
initialDrilldownWizardConfig,
mode = 'create',
onDelete = () => {},
showWelcomeMessage = true,
onWelcomeHideClick,
drilldownActionFactories,
actionFactoryPlaceContext,
docsLink,
triggerPickerDocsLink,
getTrigger,
supportedTriggers,
}: FlyoutDrilldownWizardProps<CurrentActionConfig>) {
const [
wizardConfig,
{ setActionFactory, setActionConfig, setName, setSelectedTriggers },
] = useWizardConfigState(
{ ...actionFactoryPlaceContext, triggers: supportedTriggers },
initialDrilldownWizardConfig
);
const actionFactoryContext: BaseActionFactoryContext = useMemo(
() => ({
...actionFactoryPlaceContext,
triggers: wizardConfig.selectedTriggers ?? [],
}),
[actionFactoryPlaceContext, wizardConfig.selectedTriggers]
);
const isActionValid = (
config: DrilldownWizardConfig
): config is Required<DrilldownWizardConfig> => {
if (!wizardConfig.name) return false;
if (!wizardConfig.actionFactory) return false;
if (!wizardConfig.actionConfig) return false;
if (!wizardConfig.selectedTriggers || wizardConfig.selectedTriggers.length === 0) return false;
return wizardConfig.actionFactory.isConfigValid(
wizardConfig.actionConfig,
actionFactoryContext
);
};
const footer = (
<EuiButton
onClick={() => {
if (isActionValid(wizardConfig)) {
onSubmit(wizardConfig);
}
}}
fill
isDisabled={!isActionValid(wizardConfig)}
data-test-subj={'drilldownWizardSubmit'}
>
{mode === 'edit' ? txtEditDrilldownButtonLabel : txtCreateDrilldownButtonLabel}
</EuiButton>
);
return (
<FlyoutFrame
title={mode === 'edit' ? txtEditDrilldownTitle : txtCreateDrilldownTitle}
footer={footer}
onClose={onClose}
onBack={onBack}
banner={
showWelcomeMessage && (
<DrilldownHelloBar docsLink={docsLink} onHideClick={onWelcomeHideClick} />
)
}
>
<FormDrilldownWizard
name={wizardConfig.name}
onNameChange={setName}
actionConfig={wizardConfig.actionConfig}
onActionConfigChange={setActionConfig}
currentActionFactory={wizardConfig.actionFactory}
onActionFactoryChange={setActionFactory}
actionFactories={drilldownActionFactories}
actionFactoryContext={actionFactoryContext}
onSelectedTriggersChange={setSelectedTriggers}
triggers={supportedTriggers}
getTriggerInfo={getTrigger}
triggerPickerDocsLink={triggerPickerDocsLink}
/>
{mode === 'edit' && (
<>
<EuiSpacer size={'xl'} />
<EuiButton onClick={onDelete} color={'danger'}>
{txtDeleteDrilldownButtonLabel}
</EuiButton>
</>
)}
</FlyoutFrame>
);
}

View file

@ -1,23 +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 * as React from 'react';
import { EuiFlyout } from '@elastic/eui';
import { storiesOf } from '@storybook/react';
import { FlyoutListManageDrilldowns } from './flyout_list_manage_drilldowns';
storiesOf('components/FlyoutListManageDrilldowns', module).add('default', () => (
<EuiFlyout onClose={() => {}}>
<FlyoutListManageDrilldowns
drilldowns={[
{ id: '1', actionName: 'Dashboard', drilldownName: 'Drilldown 1' },
{ id: '2', actionName: 'Dashboard', drilldownName: 'Drilldown 2' },
{ id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3', error: 'Some error...' },
]}
/>
</EuiFlyout>
));

View file

@ -1,56 +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 from 'react';
import { FlyoutFrame } from '../flyout_frame';
import { DrilldownListItem, ListManageDrilldowns } from '../list_manage_drilldowns';
import { txtManageDrilldowns } from './i18n';
import { DrilldownHelloBar } from '../drilldown_hello_bar';
export interface FlyoutListManageDrilldownsProps {
docsLink?: string;
drilldowns: DrilldownListItem[];
onClose?: () => void;
onCreate?: () => void;
onEdit?: (drilldownId: string) => void;
onDelete?: (drilldownIds: string[]) => void;
showWelcomeMessage?: boolean;
onWelcomeHideClick?: () => void;
showTriggerColumn?: boolean;
}
export function FlyoutListManageDrilldowns({
docsLink,
drilldowns,
onClose = () => {},
onCreate,
onDelete,
onEdit,
showWelcomeMessage = true,
onWelcomeHideClick,
showTriggerColumn,
}: FlyoutListManageDrilldownsProps) {
return (
<FlyoutFrame
title={txtManageDrilldowns}
onClose={onClose}
banner={
showWelcomeMessage && (
<DrilldownHelloBar docsLink={docsLink} onHideClick={onWelcomeHideClick} />
)
}
>
<ListManageDrilldowns
drilldowns={drilldowns}
onCreate={onCreate}
onEdit={onEdit}
onDelete={onDelete}
showTriggerColumn={showTriggerColumn}
/>
</FlyoutFrame>
);
}

View file

@ -1,38 +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 * as React from 'react';
import { storiesOf } from '@storybook/react';
import { FormDrilldownWizard } from './index';
import { Trigger } from '../../../../../../../src/plugins/ui_actions/public';
const otherProps = {
triggers: ['VALUE_CLICK_TRIGGER', 'SELECT_RANGE_TRIGGER', 'FILTER_TRIGGER'],
getTriggerInfo: (id: string) => ({ id } as Trigger),
onSelectedTriggersChange: () => {},
actionFactoryContext: { triggers: [] as string[] },
};
const DemoEditName: React.FC = () => {
const [name, setName] = React.useState('');
return (
<>
<FormDrilldownWizard name={name} onNameChange={setName} {...otherProps} />{' '}
<div>name: {name}</div>
</>
);
};
storiesOf('components/FormDrilldownWizard', module)
.add('default', () => {
return <FormDrilldownWizard {...otherProps} />;
})
.add('[name=foobar]', () => {
return <FormDrilldownWizard name={'foobar'} {...otherProps} />;
})
.add('can edit name', () => <DemoEditName />);

View file

@ -1,73 +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 from 'react';
import { render } from 'react-dom';
import { FormDrilldownWizard } from './form_drilldown_wizard';
import { render as renderTestingLibrary, fireEvent } from '@testing-library/react';
import { txtNameOfDrilldown } from './i18n';
import { Trigger } from '../../../../../../../src/plugins/ui_actions/public';
const otherProps = {
actionFactoryContext: { triggers: [] as string[] },
triggers: ['VALUE_CLICK_TRIGGER', 'SELECT_RANGE_TRIGGER', 'FILTER_TRIGGER'] as string[],
getTriggerInfo: (id: string) => ({ id } as Trigger),
onSelectedTriggersChange: () => {},
};
describe('<FormDrilldownWizard>', () => {
test('renders without crashing', () => {
const div = document.createElement('div');
render(<FormDrilldownWizard onNameChange={() => {}} {...otherProps} />, div);
});
describe('[name=]', () => {
test('if name not provided, uses to empty string', () => {
const div = document.createElement('div');
render(<FormDrilldownWizard {...otherProps} />, div);
const input = div.querySelector('[data-test-subj="drilldownNameInput"]') as HTMLInputElement;
expect(input?.value).toBe('');
});
test('can set initial name input field value', () => {
const div = document.createElement('div');
render(<FormDrilldownWizard name={'foo'} {...otherProps} />, div);
const input = div.querySelector('[data-test-subj="drilldownNameInput"]') as HTMLInputElement;
expect(input?.value).toBe('foo');
render(<FormDrilldownWizard name={'bar'} {...otherProps} />, div);
expect(input?.value).toBe('bar');
});
test('fires onNameChange callback on name change', () => {
const onNameChange = jest.fn();
const utils = renderTestingLibrary(
<FormDrilldownWizard name={''} onNameChange={onNameChange} {...otherProps} />
);
const input = utils.getByLabelText(txtNameOfDrilldown);
expect(onNameChange).toHaveBeenCalledTimes(0);
fireEvent.change(input, { target: { value: 'qux' } });
expect(onNameChange).toHaveBeenCalledTimes(1);
expect(onNameChange).toHaveBeenCalledWith('qux');
fireEvent.change(input, { target: { value: 'quxx' } });
expect(onNameChange).toHaveBeenCalledTimes(2);
expect(onNameChange).toHaveBeenCalledWith('quxx');
});
});
});

View file

@ -1,143 +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 from 'react';
import { EuiFieldText, EuiForm, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import { EuiCallOut } from '@elastic/eui';
import { EuiCode } from '@elastic/eui';
import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n';
import {
ActionFactory,
BaseActionConfig,
BaseActionFactoryContext,
} from '../../../dynamic_actions';
import { ActionWizard } from '../../../components/action_wizard';
import { Trigger } from '../../../../../../../src/plugins/ui_actions/public';
import { txtGetMoreActions } from './i18n';
const GET_MORE_ACTIONS_LINK = 'https://www.elastic.co/subscriptions';
const noopFn = () => {};
export interface FormDrilldownWizardProps<
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
> {
name?: string;
onNameChange?: (name: string) => void;
currentActionFactory?: ActionFactory;
onActionFactoryChange?: (actionFactory?: ActionFactory) => void;
actionFactoryContext: ActionFactoryContext;
actionConfig?: BaseActionConfig;
onActionConfigChange?: (config: BaseActionConfig) => void;
actionFactories?: ActionFactory[];
/**
* Trigger selection has changed
* @param triggers
*/
onSelectedTriggersChange: (triggers?: string[]) => void;
getTriggerInfo: (triggerId: string) => Trigger;
/**
* List of possible triggers in current context
*/
triggers: string[];
triggerPickerDocsLink?: string;
}
export const FormDrilldownWizard: React.FC<FormDrilldownWizardProps> = ({
name = '',
actionConfig,
currentActionFactory,
onNameChange = noopFn,
onActionConfigChange = noopFn,
onActionFactoryChange = noopFn,
actionFactories = [],
actionFactoryContext,
onSelectedTriggersChange,
getTriggerInfo,
triggers,
triggerPickerDocsLink,
}) => {
if (!triggers || !triggers.length) {
// Below callout is not translated, because this message is only for developers.
return (
<EuiCallOut title="Sorry, there was an error" color="danger" iconType="alert">
<p>
No triggers provided in <EuiCode>trigger</EuiCode> prop.
</p>
</EuiCallOut>
);
}
const nameFragment = (
<EuiFormRow label={txtNameOfDrilldown}>
<EuiFieldText
name="drilldown_name"
placeholder={txtUntitledDrilldown}
value={name}
disabled={onNameChange === noopFn}
onChange={(event) => onNameChange(event.target.value)}
data-test-subj="drilldownNameInput"
/>
</EuiFormRow>
);
const hasNotCompatibleLicenseFactory = () =>
actionFactories?.some((f) => !f.isCompatibleLicense());
const renderGetMoreActionsLink = () => (
<EuiText size="s">
<EuiLink
href={GET_MORE_ACTIONS_LINK}
target="_blank"
external
data-test-subj={'getMoreActionsLink'}
>
{txtGetMoreActions}
</EuiLink>
</EuiText>
);
const actionWizard = (
<EuiFormRow
label={actionFactories?.length > 1 ? txtDrilldownAction : undefined}
fullWidth={true}
labelAppend={
!currentActionFactory && hasNotCompatibleLicenseFactory() && renderGetMoreActionsLink()
}
>
<ActionWizard
actionFactories={actionFactories}
currentActionFactory={currentActionFactory}
config={actionConfig}
onActionFactoryChange={(actionFactory) => onActionFactoryChange(actionFactory)}
onConfigChange={(config) => onActionConfigChange(config)}
context={actionFactoryContext}
onSelectedTriggersChange={onSelectedTriggersChange}
getTriggerInfo={getTriggerInfo}
triggers={triggers}
triggerPickerDocsLink={triggerPickerDocsLink}
/>
</EuiFormRow>
);
return (
<>
<EuiForm>
{nameFragment}
<EuiSpacer size={'xl'} />
{actionWizard}
</EuiForm>
</>
);
};

View file

@ -1,36 +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 { i18n } from '@kbn/i18n';
export const txtNameOfDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.FormCreateDrilldown.nameOfDrilldown',
{
defaultMessage: 'Name',
}
);
export const txtUntitledDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.FormCreateDrilldown.untitledDrilldown',
{
defaultMessage: 'Untitled drilldown',
}
);
export const txtDrilldownAction = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.FormCreateDrilldown.drilldownAction',
{
defaultMessage: 'Action',
}
);
export const txtGetMoreActions = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.FormDrilldownWizard.getMoreActionsLinkLabel',
{
defaultMessage: 'Get more actions',
}
);

View file

@ -1,40 +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 { i18n } from '@kbn/i18n';
export const txtCreateDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.createDrilldownButtonLabel',
{
defaultMessage: 'Create new',
}
);
export const txtEditDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.editDrilldownButtonLabel',
{
defaultMessage: 'Edit',
}
);
export const txtDeleteDrilldowns = (count: number) =>
i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.deleteDrilldownsButtonLabel',
{
defaultMessage: 'Delete ({count})',
values: {
count,
},
}
);
export const txtSelectDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.ListManageDrilldowns.selectThisDrilldownCheckboxLabel',
{
defaultMessage: 'Select this drilldown',
}
);

View file

@ -1,37 +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 * as React from 'react';
import { storiesOf } from '@storybook/react';
import { ListManageDrilldowns } from './list_manage_drilldowns';
storiesOf('components/ListManageDrilldowns', module).add('default', () => (
<ListManageDrilldowns
drilldowns={[
{
id: '1',
actionName: 'Dashboard',
drilldownName: 'Drilldown 1',
icon: 'dashboardApp',
triggers: [{ title: 'trigger' }],
},
{
id: '2',
actionName: 'Dashboard',
drilldownName: 'Drilldown 2',
icon: 'dashboardApp',
triggers: [{ title: 'trigger' }],
},
{
id: '3',
actionName: 'Dashboard',
drilldownName: 'Drilldown 3',
triggers: [{ title: 'trigger', description: 'trigger' }],
},
]}
/>
));

View file

@ -1,16 +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 { BaseActionFactoryContext } from '../../dynamic_actions';
/**
* Interface used as piece of ActionFactoryContext that is passed in from drilldown wizard component to action factories
* Omitted values are added inside the wizard and then full {@link BaseActionFactoryContext} passed into action factory methods
*/
export type ActionFactoryPlaceContext<
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
> = Omit<ActionFactoryContext, 'triggers'>;

View file

@ -0,0 +1,12 @@
# Drilldown Manager
Drilldown Manager is the flyout that opens where drilldowns can be managed using
a CRUD UI. (It does not necessarily need to be a flyout, you can also embed it
directly on a page.)
The main React component that this folder exports is `<DrilldownManager>`, which
should normally be rendered in a flyout.
A new instance of Drilldown Manager is rendered for every place where drilldowns
are used. For example, for each panel on the dashboard a separate new Drilldown
Manager is rendered in the flyout.

View file

@ -0,0 +1,132 @@
/*
* 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 {
EuiBetaBadge,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiIcon,
EuiLink,
EuiText,
} from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
const txtDrilldownAction = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownForm.drilldownAction',
{
defaultMessage: 'Action',
}
);
const txtGetMoreActions = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownForm.getMoreActionsLinkLabel',
{
defaultMessage: 'Get more actions',
}
);
const txtBetaActionFactoryLabel = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownForm.betaActionLabel',
{
defaultMessage: `Beta`,
}
);
const txtBetaActionFactoryTooltip = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownForm.betaActionTooltip',
{
defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting any bugs or providing other feedback.`,
}
);
const txtChangeButton = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownForm.changeButton',
{
defaultMessage: 'Change',
}
);
const GET_MORE_ACTIONS_LINK = 'https://www.elastic.co/subscriptions';
const moreActions = (
<EuiText size="s">
<EuiLink
href={GET_MORE_ACTIONS_LINK}
target="_blank"
external
data-test-subj={'getMoreActionsLink'}
>
{txtGetMoreActions}
</EuiLink>
</EuiText>
);
export interface ActionFactoryProps {
/** Action factory name. */
name?: string;
/** ID of EUI icon. */
icon?: string;
/** Whether the current drilldown type is in beta. */
beta?: boolean;
/** Whether to show "Get more actions" link to upgrade license. */
showMoreLink?: boolean;
/** On drilldown type change click. */
onChange?: () => void;
}
export const ActionFactory: React.FC<ActionFactoryProps> = ({
name,
icon,
beta,
showMoreLink,
onChange,
}) => {
return (
<EuiFormRow
label={txtDrilldownAction}
fullWidth={true}
labelAppend={showMoreLink && moreActions}
>
<header>
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="s">
{!!icon && (
<EuiFlexItem grow={false}>
<EuiIcon type={icon} size="m" />
</EuiFlexItem>
)}
<EuiFlexItem grow={true}>
<EuiText>
<h4>
{name}{' '}
{beta && (
<EuiBetaBadge
label={txtBetaActionFactoryLabel}
tooltipContent={txtBetaActionFactoryTooltip}
/>
)}
</h4>
</EuiText>
</EuiFlexItem>
{!!onChange && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty size="xs" onClick={onChange}>
{txtChangeButton}
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
</header>
</EuiFormRow>
);
};

View file

@ -0,0 +1,27 @@
/*
* 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 * as React from 'react';
import { EuiButton } from '@elastic/eui';
export interface ButtonSubmitProps {
disabled?: boolean;
onClick: () => void;
}
export const ButtonSubmit: React.FC<ButtonSubmitProps> = ({ disabled, onClick, children }) => {
return (
<EuiButton
fill
isDisabled={disabled}
data-test-subj={'drilldownWizardSubmit'}
onClick={onClick}
>
{children}
</EuiButton>
);
};

View file

@ -0,0 +1,8 @@
/*
* 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 './button_submit';

View file

@ -0,0 +1,62 @@
/*
* 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 * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { DrilldownForm } from '.';
import type { TriggerPickerProps } from '../trigger_picker';
const triggers: TriggerPickerProps = {
items: [
{
id: 'RANGE_SELECT_TRIGGER',
title: 'Range selected',
description: 'On chart brush.',
},
{
id: 'VALUE_CLICK_TRIGGER',
title: 'Value click',
description: 'On point click in chart',
},
],
selected: ['RANGE_SELECT_TRIGGER'],
docs: 'http://example.com',
onChange: () => {},
};
storiesOf('components/DrilldownForm', module)
.add('Default', () => {
return (
<DrilldownForm name={'...'} triggers={triggers} onNameChange={action('onNameChange')}>
children...
</DrilldownForm>
);
})
.add('With license link', () => {
return (
<DrilldownForm name={'...'} triggers={triggers} onNameChange={action('onNameChange')}>
children...
</DrilldownForm>
);
})
.add('No triggers', () => {
return (
<DrilldownForm
name={'...'}
triggers={{
items: [],
selected: ['RANGE_SELECT_TRIGGER'],
docs: 'http://example.com',
onChange: () => {},
}}
onNameChange={action('onNameChange')}
>
children...
</DrilldownForm>
);
});

View file

@ -0,0 +1,92 @@
/*
* 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 { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiCallOut, EuiCode } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TriggerPicker, TriggerPickerProps } from '../trigger_picker';
const txtNameOfDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownForm.nameOfDrilldown',
{
defaultMessage: 'Name',
}
);
const txtUntitledDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownForm.untitledDrilldown',
{
defaultMessage: 'Untitled drilldown',
}
);
const txtTrigger = i18n.translate('xpack.uiActionsEnhanced.components.DrilldownForm.trigger', {
defaultMessage: 'Trigger',
});
export interface FormDrilldownWizardProps {
/** Value of name field. */
name?: string;
/** Callback called on name change. */
onNameChange?: (name: string) => void;
/** Trigger picker props. */
triggers?: TriggerPickerProps;
/** Whether the form elements should be disabled. */
disabled?: boolean;
}
export const DrilldownForm: React.FC<FormDrilldownWizardProps> = ({
name = '',
onNameChange,
triggers,
disabled,
children,
}) => {
if (!!triggers && !triggers.items.length) {
// Below callout is not translated, because this message is only for developers.
return (
<EuiCallOut title="Sorry, there was an error" color="danger" iconType="alert">
<p>
No triggers provided in <EuiCode>triggers</EuiCode> prop.
</p>
</EuiCallOut>
);
}
const nameFragment = (
<EuiFormRow label={txtNameOfDrilldown}>
<EuiFieldText
name="drilldown_name"
placeholder={txtUntitledDrilldown}
value={name}
disabled={!onNameChange || disabled}
onChange={!!onNameChange ? (event) => onNameChange(event.target.value) : undefined}
data-test-subj="drilldownNameInput"
/>
</EuiFormRow>
);
const triggersFragment = !!triggers && triggers.items.length > 1 && (
<EuiFormRow label={txtTrigger} fullWidth={true}>
<TriggerPicker {...triggers} disabled={disabled} />
</EuiFormRow>
);
return (
<EuiForm data-test-subj={`DrilldownForm`}>
<EuiSpacer size={'m'} />
{nameFragment}
<EuiSpacer size={'m'} />
{triggersFragment}
<EuiSpacer size={'m'} />
<div>{children}</div>
</EuiForm>
);
};

View file

@ -26,10 +26,7 @@ export interface DrilldownHelloBarProps {
export const WELCOME_MESSAGE_TEST_SUBJ = 'drilldownsWelcomeMessage';
export const DrilldownHelloBar: React.FC<DrilldownHelloBarProps> = ({
docsLink,
onHideClick = () => {},
}) => {
export const DrilldownHelloBar: React.FC<DrilldownHelloBarProps> = ({ docsLink, onHideClick }) => {
return (
<EuiCallOut data-test-subj={WELCOME_MESSAGE_TEST_SUBJ}>
<EuiFlexGroup responsive={false}>
@ -49,11 +46,13 @@ export const DrilldownHelloBar: React.FC<DrilldownHelloBarProps> = ({
</>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty size="xs" onClick={onHideClick}>
{txtHideHelpButtonLabel}
</EuiButtonEmpty>
</EuiFlexItem>
{!!onHideClick && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty size="xs" onClick={onHideClick}>
{txtHideHelpButtonLabel}
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiCallOut>
);

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { DrilldownTable } from './drilldown_table';
import { FlyoutFrame } from '../flyout_frame';
storiesOf('components/ListManageDrilldowns', module)
.add('Default', () => (
<DrilldownTable
items={[
{
id: '1',
actionName: 'Dashboard',
drilldownName: 'Drilldown 1',
icon: 'dashboardApp',
triggers: [{ title: 'trigger' }],
},
{
id: '2',
actionName: 'Dashboard',
drilldownName: 'Drilldown 2',
icon: 'dashboardApp',
triggers: [{ title: 'trigger' }],
},
{
id: '3',
actionName: 'Dashboard',
drilldownName: 'Drilldown 3',
triggers: [{ title: 'trigger', description: 'trigger' }],
},
]}
onCreate={action('onCreate')}
onDelete={action('onDelete')}
onEdit={action('onEdit')}
/>
))
.add('Empty list', () => (
<DrilldownTable
items={[]}
onCreate={action('onCreate')}
onDelete={action('onDelete')}
onEdit={action('onEdit')}
/>
))
.add('A single drilldown', () => (
<DrilldownTable
items={[
{
id: '1',
actionName: 'Dashboard',
drilldownName: 'Drilldown 1',
icon: 'dashboardApp',
triggers: [{ title: 'trigger' }],
},
]}
onCreate={action('onCreate')}
onDelete={action('onDelete')}
onEdit={action('onEdit')}
/>
))
.add('Inside a flyout frame', () => (
<FlyoutFrame title={'Some Title'} onClose={action('onClose')} banner={null}>
<DrilldownTable
items={[
{ id: '1', actionName: 'Dashboard', drilldownName: 'Drilldown 1' },
{ id: '2', actionName: 'Dashboard', drilldownName: 'Drilldown 2' },
{
id: '3',
actionName: 'Dashboard',
drilldownName: 'Drilldown 3',
error: 'Some error...',
},
]}
onCreate={action('onCreate')}
onDelete={action('onDelete')}
onEdit={action('onEdit')}
/>
</FlyoutFrame>
));

View file

@ -7,26 +7,22 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import {
DrilldownListItem,
ListManageDrilldowns,
TEST_SUBJ_DRILLDOWN_ITEM,
} from './list_manage_drilldowns';
import { DrilldownTable, DrilldownTableItem, TEST_SUBJ_DRILLDOWN_ITEM } from './drilldown_table';
const drilldowns: DrilldownListItem[] = [
const drilldowns: DrilldownTableItem[] = [
{ id: '1', actionName: 'Dashboard', drilldownName: 'Drilldown 1' },
{ id: '2', actionName: 'Dashboard', drilldownName: 'Drilldown 2' },
{ id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3', error: 'an error' },
];
test('Render list of drilldowns', () => {
const screen = render(<ListManageDrilldowns drilldowns={drilldowns} />);
const screen = render(<DrilldownTable items={drilldowns} />);
expect(screen.getAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(drilldowns.length);
});
test('Emit onEdit() when clicking on edit drilldown', () => {
const fn = jest.fn();
const screen = render(<ListManageDrilldowns drilldowns={drilldowns} onEdit={fn} />);
const screen = render(<DrilldownTable items={drilldowns} onEdit={fn} />);
const editButtons = screen.getAllByText('Edit');
expect(editButtons).toHaveLength(drilldowns.length);
@ -36,21 +32,21 @@ test('Emit onEdit() when clicking on edit drilldown', () => {
test('Emit onCreate() when clicking on create drilldown', () => {
const fn = jest.fn();
const screen = render(<ListManageDrilldowns drilldowns={drilldowns} onCreate={fn} />);
const screen = render(<DrilldownTable items={drilldowns} onCreate={fn} />);
fireEvent.click(screen.getByText('Create new'));
expect(fn).toBeCalled();
});
test('Delete button is not visible when non is selected', () => {
const fn = jest.fn();
const screen = render(<ListManageDrilldowns drilldowns={drilldowns} onCreate={fn} />);
const screen = render(<DrilldownTable items={drilldowns} onCreate={fn} />);
expect(screen.queryByText(/Delete/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Create/i)).toBeInTheDocument();
});
test('Can delete drilldowns', () => {
const fn = jest.fn();
const screen = render(<ListManageDrilldowns drilldowns={drilldowns} onDelete={fn} />);
const screen = render(<DrilldownTable items={drilldowns} onDelete={fn} />);
const checkboxes = screen.getAllByLabelText(/Select this drilldown/i);
expect(checkboxes).toHaveLength(3);
@ -66,6 +62,6 @@ test('Can delete drilldowns', () => {
});
test('Error is displayed', () => {
const screen = render(<ListManageDrilldowns drilldowns={drilldowns} />);
const screen = render(<DrilldownTable items={drilldowns} />);
expect(screen.getByLabelText('an error')).toBeInTheDocument();
});

View file

@ -6,32 +6,36 @@
*/
import {
EuiBasicTable,
EuiInMemoryTable,
EuiBasicTableColumn,
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiSpacer,
EuiTextColor,
EuiToolTip,
} from '@elastic/eui';
import React, { useState } from 'react';
import { TextWithIcon } from '../text_with_icon';
import { TriggerLineItem } from '../trigger_line_item';
import {
txtCreateDrilldown,
txtDeleteDrilldowns,
txtEditDrilldown,
txtCloneDrilldown,
txtSelectDrilldown,
txtName,
txtAction,
txtTrigger,
} from './i18n';
export interface DrilldownListItem {
export interface DrilldownTableItem {
id: string;
actionName: string;
drilldownName: string;
icon?: string;
error?: string;
triggers?: Trigger[];
triggerIncompatible?: boolean;
}
interface Trigger {
@ -39,36 +43,34 @@ interface Trigger {
description?: string;
}
export interface ListManageDrilldownsProps {
drilldowns: DrilldownListItem[];
onEdit?: (id: string) => void;
onCreate?: () => void;
onDelete?: (ids: string[]) => void;
showTriggerColumn?: boolean;
}
const noop = () => {};
export const TEST_SUBJ_DRILLDOWN_ITEM = 'listManageDrilldownsItem';
export function ListManageDrilldowns({
drilldowns,
onEdit = noop,
onCreate = noop,
onDelete = noop,
showTriggerColumn = true,
}: ListManageDrilldownsProps) {
export interface DrilldownTableProps {
items: DrilldownTableItem[];
onCreate?: () => void;
onDelete?: (ids: string[]) => void;
onEdit?: (id: string) => void;
onCopy?: (id: string) => void;
}
export const DrilldownTable: React.FC<DrilldownTableProps> = ({
items: drilldowns,
onCreate,
onDelete,
onEdit,
onCopy,
}) => {
const [selectedDrilldowns, setSelectedDrilldowns] = useState<string[]>([]);
const columns: Array<EuiBasicTableColumn<DrilldownListItem>> = [
const columns: Array<EuiBasicTableColumn<DrilldownTableItem>> = [
{
name: 'Name',
field: 'drilldownName',
name: txtName,
sortable: true,
'data-test-subj': 'drilldownListItemName',
render: (drilldown: DrilldownListItem) => (
render: (drilldownName: string, drilldown: DrilldownTableItem) => (
<div>
{drilldown.drilldownName}{' '}
{drilldownName}{' '}
{drilldown.error && (
<EuiToolTip id={`drilldownError-${drilldown.id}`} content={drilldown.error}>
<EuiIcon
@ -85,50 +87,62 @@ export function ListManageDrilldowns({
),
},
{
name: 'Action',
render: (drilldown: DrilldownListItem) => (
<EuiFlexGroup responsive={false} alignItems="center" gutterSize={'s'}>
{drilldown.icon && (
<EuiFlexItem grow={false}>
<EuiIcon type={drilldown.icon} />
</EuiFlexItem>
)}
<EuiFlexItem grow={false} style={{ flexWrap: 'wrap' }}>
<EuiTextColor color="subdued">{drilldown.actionName}</EuiTextColor>
</EuiFlexItem>
</EuiFlexGroup>
name: txtAction,
render: (drilldown: DrilldownTableItem) => (
<TextWithIcon icon={drilldown.icon} color={'subdued'}>
{drilldown.actionName}
</TextWithIcon>
),
},
showTriggerColumn && {
name: 'Trigger',
{
field: 'triggers',
name: txtTrigger,
textOnly: true,
render: (drilldown: DrilldownListItem) =>
drilldown.triggers?.map((trigger, idx) =>
trigger.description ? (
<EuiToolTip content={trigger.description} key={idx}>
<EuiTextColor color="subdued">{trigger.title ?? 'unknown'}</EuiTextColor>
</EuiToolTip>
) : (
<EuiTextColor color="subdued" key={idx}>
{trigger.title ?? 'unknown'}
</EuiTextColor>
)
),
sortable: (drilldown: DrilldownTableItem) =>
drilldown.triggers ? drilldown.triggers[0].title : '',
render: (triggers: unknown, drilldown: DrilldownTableItem) => {
if (!drilldown.triggers) return null;
const trigger = drilldown.triggers[0];
return (
<TriggerLineItem
incompatible={drilldown.triggerIncompatible}
tooltip={trigger.description}
>
{trigger.title ?? 'unknown'}
</TriggerLineItem>
);
},
},
{
align: 'right',
width: '64px',
render: (drilldown: DrilldownListItem) => (
<EuiButtonEmpty size="xs" onClick={() => onEdit(drilldown.id)}>
{txtEditDrilldown}
</EuiButtonEmpty>
render: (drilldown: DrilldownTableItem) => (
<>
{!!onEdit && (
<EuiButtonEmpty
size="xs"
disabled={!!selectedDrilldowns.length}
onClick={() => onEdit(drilldown.id)}
>
{txtEditDrilldown}
</EuiButtonEmpty>
)}
{!!onCopy && (
<EuiButtonEmpty
size="xs"
disabled={!!selectedDrilldowns.length}
onClick={() => onCopy(drilldown.id)}
>
{txtCloneDrilldown}
</EuiButtonEmpty>
)}
</>
),
},
].filter(Boolean) as Array<EuiBasicTableColumn<DrilldownListItem>>;
].filter(Boolean) as Array<EuiBasicTableColumn<DrilldownTableItem>>;
return (
<>
<EuiBasicTable
<EuiInMemoryTable
items={drilldowns}
itemId="id"
columns={columns}
@ -144,13 +158,20 @@ export function ListManageDrilldowns({
'data-test-subj': TEST_SUBJ_DRILLDOWN_ITEM,
}}
hasActions={true}
sorting={{
sort: {
field: 'drilldownName',
direction: 'asc',
},
}}
/>
<EuiSpacer />
{selectedDrilldowns.length === 0 ? (
{!!onCreate && !selectedDrilldowns.length && (
<EuiButton fill onClick={() => onCreate()}>
{txtCreateDrilldown}
</EuiButton>
) : (
)}
{!!onDelete && selectedDrilldowns.length > 0 && (
<EuiButton
color="danger"
fill
@ -162,4 +183,4 @@ export function ListManageDrilldowns({
)}
</>
);
}
};

View file

@ -0,0 +1,65 @@
/*
* 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';
export const txtCreateDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTable.createDrilldownButtonLabel',
{
defaultMessage: 'Create new',
}
);
export const txtEditDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTable.editDrilldownButtonLabel',
{
defaultMessage: 'Edit',
}
);
export const txtCloneDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTable.copyDrilldownButtonLabel',
{
defaultMessage: 'Copy',
}
);
export const txtDeleteDrilldowns = (count: number) =>
i18n.translate('xpack.uiActionsEnhanced.components.DrilldownTable.deleteDrilldownsButtonLabel', {
defaultMessage: 'Delete ({count})',
values: {
count,
},
});
export const txtSelectDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTable.selectThisDrilldownCheckboxLabel',
{
defaultMessage: 'Select this drilldown',
}
);
export const txtName = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTable.nameColumnTitle',
{
defaultMessage: 'Name',
}
);
export const txtAction = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTable.actionColumnTitle',
{
defaultMessage: 'Action',
}
);
export const txtTrigger = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTable.triggerColumnTitle',
{
defaultMessage: 'Trigger',
}
);

View file

@ -0,0 +1,8 @@
/*
* 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 './drilldown_table';

View file

@ -0,0 +1,137 @@
/*
* 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, { useState } from 'react';
import {
EuiInMemoryTable,
EuiBasicTableColumn,
EuiButtonEmpty,
EuiSpacer,
EuiButton,
EuiText,
EuiSearchBarProps,
} from '@elastic/eui';
import {
txtNameColumnTitle,
txtSelectableMessage,
txtCopyButtonLabel,
txtSingleItemCopyActionLabel,
txtActionColumnTitle,
txtTriggerColumnTitle,
} from './i18n';
import { TextWithIcon } from '../text_with_icon';
import { TriggerLineItem } from '../trigger_line_item';
export interface DrilldownTemplateTableItem {
id: string;
name: string;
icon?: string;
description?: string;
actionName?: string;
actionIcon?: string;
trigger?: string;
triggerIncompatible?: boolean;
}
export interface DrilldownTemplateTableProps {
items: DrilldownTemplateTableItem[];
onCreate?: (id: string) => void;
onClone?: (ids: string[]) => void;
}
export const DrilldownTemplateTable: React.FC<DrilldownTemplateTableProps> = ({
items,
onCreate,
onClone,
}) => {
const [selected, setSelected] = useState<string[]>([]);
const columns: Array<EuiBasicTableColumn<DrilldownTemplateTableItem>> = [
{
field: 'name',
name: txtNameColumnTitle,
sortable: true,
render: (omit, item: DrilldownTemplateTableItem) => (
<div style={{ display: 'block' }}>
<div style={{ display: 'block' }}>{item.name}</div>
<EuiText size={'xs'} color={'subdued'}>
{item.description}
</EuiText>
</div>
),
},
{
name: txtActionColumnTitle,
render: (item: DrilldownTemplateTableItem) => (
<TextWithIcon icon={item.actionIcon || 'empty'} color={'subdued'}>
{item.actionName}
</TextWithIcon>
),
},
{
field: 'trigger',
name: txtTriggerColumnTitle,
sortable: true,
render: (omit, item: DrilldownTemplateTableItem) => (
<TriggerLineItem incompatible={item.triggerIncompatible}>{item.trigger}</TriggerLineItem>
),
},
{
align: 'right',
render: (drilldown: DrilldownTemplateTableItem) =>
!!onCreate && (
<EuiButtonEmpty
size="xs"
disabled={!!selected.length}
onClick={() => onCreate(drilldown.id)}
>
{txtSingleItemCopyActionLabel}
</EuiButtonEmpty>
),
},
];
const search: EuiSearchBarProps = {
box: {
incremental: true,
},
defaultQuery: '',
};
return (
<>
<EuiInMemoryTable
itemId="id"
tableLayout={'auto'}
items={items}
columns={columns}
isSelectable={!!onClone}
responsive={false}
search={search}
sorting={{
sort: {
field: 'nameCol',
direction: 'asc',
},
}}
selection={{
onSelectionChange: (selection) => {
setSelected(selection.map((drilldown) => drilldown.id));
},
selectableMessage: () => txtSelectableMessage,
}}
hasActions={true}
/>
<EuiSpacer />
{!!onClone && !!selected.length && (
<EuiButton fill onClick={() => onClone(selected)}>
{txtCopyButtonLabel(selected.length)}
</EuiButton>
)}
</>
);
};

View file

@ -0,0 +1,62 @@
/*
* 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';
export const txtSelectableMessage = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.selectableMessage',
{
defaultMessage: 'Select this template',
}
);
export const txtNameColumnTitle = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.nameColumnTitle',
{
defaultMessage: 'Name',
description: 'Title of the first column in drilldown template cloning table.',
}
);
export const txtSourceColumnTitle = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.sourceColumnTitle',
{
defaultMessage: 'Panel',
description: 'Column title which describes from where the drilldown is cloned.',
}
);
export const txtActionColumnTitle = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.actionColumnTitle',
{
defaultMessage: 'Action',
}
);
export const txtTriggerColumnTitle = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.triggerColumnTitle',
{
defaultMessage: 'Trigger',
}
);
export const txtSingleItemCopyActionLabel = i18n.translate(
'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.singleItemCopyAction',
{
defaultMessage: 'Copy',
description: '"Copy" action button label in drilldown template cloning table last column.',
}
);
export const txtCopyButtonLabel = (count: number) =>
i18n.translate('xpack.uiActionsEnhanced.components.DrilldownTemplateTable.copyButtonLabel', {
defaultMessage: 'Copy ({count})',
description: 'Label of drilldown template table bottom copy button.',
values: {
count,
},
});

View file

@ -0,0 +1,8 @@
/*
* 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 './drilldown_template_table';

View file

@ -0,0 +1,8 @@
/*
* 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 './text_with_icon';

View file

@ -0,0 +1,60 @@
/*
* 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 * as React from 'react';
import {
EuiTextColor,
EuiTextColorProps,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiToolTip,
} from '@elastic/eui';
export interface TextWithIconProps {
color?: EuiTextColorProps['color'];
tooltip?: React.ReactNode;
icon?: string;
iconColor?: string;
iconTooltip?: React.ReactNode;
}
export const TextWithIcon: React.FC<TextWithIconProps> = ({
color,
tooltip,
icon,
iconColor,
iconTooltip,
children,
}) => {
return (
<EuiFlexGroup responsive={false} alignItems="center" gutterSize={'s'}>
{!!icon && (
<EuiFlexItem grow={false}>
{!!iconTooltip ? (
<EuiToolTip content={iconTooltip}>
<EuiIcon color={iconColor} type={icon} />
</EuiToolTip>
) : (
<EuiIcon color={iconColor} type={icon} />
)}
</EuiFlexItem>
)}
{!!children && (
<EuiFlexItem grow={false} style={{ flexWrap: 'wrap' }}>
{tooltip ? (
<EuiToolTip content={tooltip}>
<EuiTextColor color={color}>{children}</EuiTextColor>
</EuiToolTip>
) : (
<EuiTextColor color={color}>{children}</EuiTextColor>
)}
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,8 @@
/*
* 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 './trigger_line_item';

View file

@ -0,0 +1,40 @@
/*
* 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 * as React from 'react';
import { i18n } from '@kbn/i18n';
import { TextWithIcon } from '../text_with_icon';
export const txtIncompatibleTooltip = i18n.translate(
'xpack.uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip',
{
defaultMessage: 'This trigger type not supported by this panel',
}
);
export interface TriggerLineItemProps {
tooltip?: React.ReactNode;
incompatible?: boolean;
}
export const TriggerLineItem: React.FC<TriggerLineItemProps> = ({
tooltip,
incompatible,
children,
}) => {
return (
<TextWithIcon
color={'subdued'}
tooltip={tooltip}
icon={incompatible ? 'alert' : undefined}
iconColor={incompatible ? 'danger' : undefined}
iconTooltip={incompatible ? txtIncompatibleTooltip : undefined}
>
{children}
</TextWithIcon>
);
};

View file

@ -0,0 +1,9 @@
/*
* 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 { TriggerPickerItemDescription } from './trigger_picker_item';
export * from './trigger_picker';

View file

@ -0,0 +1,102 @@
/*
* 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 * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { TriggerPicker } from '.';
const Demo: React.FC = () => {
const [triggers, setTriggers] = React.useState<string[]>(['RANGE_SELECT_TRIGGER']);
return (
<TriggerPicker
docs={'http://example.com'}
items={[
{
id: 'RANGE_SELECT_TRIGGER',
title: 'Range selected',
description: 'On chart brush.',
},
{
id: 'VALUE_CLICK_TRIGGER',
title: 'Value click',
description: 'On point click in chart',
},
]}
selected={triggers}
onChange={setTriggers}
/>
);
};
storiesOf('components/TriggerPicker', module)
.add('Default', () => {
return (
<TriggerPicker
items={[
{
id: 'RANGE_SELECT_TRIGGER',
title: 'Range selected',
description: 'On chart brush.',
},
{
id: 'VALUE_CLICK_TRIGGER',
title: 'Value click',
description: 'On point click in chart',
},
]}
selected={[]}
onChange={action('onChange')}
/>
);
})
.add('With docs', () => {
return (
<TriggerPicker
docs={'http://example.com'}
items={[
{
id: 'RANGE_SELECT_TRIGGER',
title: 'Range selected',
description: 'On chart brush.',
},
{
id: 'VALUE_CLICK_TRIGGER',
title: 'Value click',
description: 'On point click in chart',
},
]}
selected={[]}
onChange={action('onChange')}
/>
);
})
.add('Selected trigger', () => {
return (
<TriggerPicker
docs={'http://example.com'}
items={[
{
id: 'RANGE_SELECT_TRIGGER',
title: 'Range selected',
description: 'On chart brush.',
},
{
id: 'VALUE_CLICK_TRIGGER',
title: 'Value click',
description: 'On point click in chart',
},
]}
selected={['VALUE_CLICK_TRIGGER']}
onChange={action('onChange')}
/>
);
})
.add('Interactive', () => {
return <Demo />;
});

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiText, EuiToolTip, EuiFormFieldset, EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TriggerPickerItemDescription, TriggerPickerItem } from './trigger_picker_item';
const txtTriggerPickerLabel = i18n.translate(
'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel',
{
defaultMessage: 'Show option on:',
}
);
const txtTriggerPickerHelpText = i18n.translate(
'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpText',
{
defaultMessage: "What's this?",
}
);
const txtTriggerPickerHelpTooltip = i18n.translate(
'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip',
{
defaultMessage: 'Determines when the drilldown appears in context menu',
}
);
export interface TriggerPickerProps {
/** List of available triggers. */
items: TriggerPickerItemDescription[];
/** List of IDs of selected triggers. */
selected?: string[];
/** Link to documentation. */
docs?: string;
/** Whether user interactions should be disabled. */
disabled?: boolean;
/** Called on trigger selection change. */
onChange: (selected: string[]) => void;
}
export const TriggerPicker: React.FC<TriggerPickerProps> = ({
items,
selected = [],
docs,
disabled,
onChange,
}) => {
return (
<EuiFormFieldset
data-test-subj={`triggerPicker`}
legend={{
children: !!docs && (
<EuiText size="s">
<h5>
<span>{txtTriggerPickerLabel}</span>{' '}
<EuiToolTip content={txtTriggerPickerHelpTooltip}>
<EuiLink href={docs} target={'blank'} external>
{txtTriggerPickerHelpText}
</EuiLink>
</EuiToolTip>
</h5>
</EuiText>
),
}}
style={{ maxWidth: `80%` }}
>
{items.map((trigger) => (
<TriggerPickerItem
key={trigger.id}
id={trigger.id}
title={trigger.title}
description={trigger.description}
checked={trigger.id === selected[0]}
disabled={disabled}
onSelect={(id) => onChange([id])}
/>
))}
</EuiFormFieldset>
);
};

View file

@ -0,0 +1,73 @@
/*
* 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 { EuiSpacer, EuiText, EuiCheckableCard, EuiTextColor, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const txtUnknown = i18n.translate('xpack.uiActionsEnhanced.components.TriggerPickerItem.unknown', {
defaultMessage: 'Unknown',
});
export interface TriggerPickerItemDescription {
id: string;
title?: string;
description?: string;
}
export interface TriggerPickerItemProps extends TriggerPickerItemDescription {
/** Whether the item is selected. */
checked?: boolean;
/** Whether to disable user interaction. */
disabled?: boolean;
/** Called when item is selected by user. */
onSelect: (id: string) => void;
}
export const TriggerPickerItem: React.FC<TriggerPickerItemProps> = ({
id,
title = txtUnknown,
description,
checked,
disabled,
onSelect,
}) => {
const descriptionFragment = !!description && (
<div>
<EuiText size={'s'}>
<EuiTextColor color={'subdued'}>{description}</EuiTextColor>
</EuiText>
</div>
);
const label = (
<>
<EuiTitle size={'xxs'}>
<span>{title}</span>
</EuiTitle>
{descriptionFragment}
</>
);
return (
<>
<EuiCheckableCard
id={id}
label={label}
name={id}
value={id}
checked={checked}
disabled={disabled}
onChange={() => onSelect(id)}
data-test-subj={`triggerPicker-${id}`}
/>
<EuiSpacer size={'s'} />
</>
);
};

View file

@ -0,0 +1,8 @@
/*
* 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 { ActionFactoryPlaceContext } from '../types';

View file

@ -0,0 +1,31 @@
/*
* 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 { ActionFactoryPicker as ActionFactoryPickerUi } from '../../../../components/action_factory_picker';
import { useDrilldownManager } from '../context';
import { ActionFactoryView } from '../action_factory_view';
export const ActionFactoryPicker: React.FC = ({}) => {
const drilldowns = useDrilldownManager();
const factory = drilldowns.useActionFactory();
const context = React.useMemo(() => drilldowns.getActionFactoryContext(), [drilldowns]);
if (!!factory) {
return <ActionFactoryView factory={factory} context={context} />;
}
return (
<ActionFactoryPickerUi
actionFactories={drilldowns.deps.actionFactories}
context={context}
onSelect={(actionFactory) => {
drilldowns.setActionFactory(actionFactory);
}}
/>
);
};

View file

@ -0,0 +1,8 @@
/*
* 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 './action_factory_picker';

View file

@ -0,0 +1,33 @@
/*
* 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 { ActionFactory as ActionFactoryUi } from '../../components/action_factory';
import { ActionFactory, BaseActionFactoryContext } from '../../../../dynamic_actions';
import { useDrilldownManager } from '../context';
export interface ActionFactoryViewProps {
factory: ActionFactory;
context: BaseActionFactoryContext;
constant?: boolean;
}
export const ActionFactoryView: React.FC<ActionFactoryViewProps> = ({
factory,
context,
constant,
}) => {
const drilldowns = useDrilldownManager();
const name = React.useMemo(() => factory.getDisplayName(context), [factory, context]);
const icon = React.useMemo(() => factory.getIconType(context), [factory, context]);
const handleChange = React.useMemo(() => {
if (constant) return undefined;
return () => drilldowns.setActionFactory(undefined);
}, [drilldowns, constant]);
return <ActionFactoryUi name={name} icon={icon} beta={factory.isBeta} onChange={handleChange} />;
};

View file

@ -0,0 +1,8 @@
/*
* 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 './action_factory_view';

View file

@ -0,0 +1,25 @@
/*
* 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 * as React from 'react';
import { DrilldownManagerState, DrilldownManagerStateDeps } from '../../state';
const context = React.createContext<DrilldownManagerState | null>(null);
export const useDrilldownManager = () => React.useContext(context)!;
export type DrilldownManagerProviderProps = DrilldownManagerStateDeps;
export const DrilldownManagerProvider: React.FC<DrilldownManagerProviderProps> = ({
children,
...deps
}) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const value = React.useMemo(() => new DrilldownManagerState(deps), []);
return <context.Provider value={value}>{children}</context.Provider>;
};

View file

@ -0,0 +1,8 @@
/*
* 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 './context';

View file

@ -0,0 +1,63 @@
/*
* 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 * as React from 'react';
import { i18n } from '@kbn/i18n';
import useMountedState from 'react-use/lib/useMountedState';
import { DrilldownManagerTitle } from '../drilldown_manager_title';
import { useDrilldownManager } from '../context';
import { ActionFactoryPicker } from '../action_factory_picker';
import { DrilldownManagerFooter } from '../drilldown_manager_footer';
import { DrilldownStateForm } from '../drilldown_state_form';
import { ButtonSubmit } from '../../components/button_submit';
const txtCreateDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.containers.createDrilldownForm.title',
{
defaultMessage: 'Create Drilldown',
description: 'Drilldowns flyout title for new drilldown form.',
}
);
const txtCreateDrilldownButton = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.containers.createDrilldownForm.primaryButton',
{
defaultMessage: 'Create drilldown',
description: 'Primary button on new drilldown creation form.',
}
);
export const CreateDrilldownForm: React.FC = () => {
const isMounted = useMountedState();
const drilldowns = useDrilldownManager();
const drilldownState = drilldowns.getDrilldownState()!;
const error = drilldownState.useError();
const [disabled, setDisabled] = React.useState(false);
const handleCreate = () => {
setDisabled(true);
drilldowns.createDrilldown().finally(() => {
if (!isMounted()) return;
setDisabled(false);
});
};
return (
<>
<DrilldownManagerTitle>{txtCreateDrilldown}</DrilldownManagerTitle>
<ActionFactoryPicker />
{!!drilldownState && <DrilldownStateForm state={drilldownState} disabled={disabled} />}
{!!drilldownState && (
<DrilldownManagerFooter>
<ButtonSubmit disabled={disabled || !!error} onClick={handleCreate}>
{txtCreateDrilldownButton}
</ButtonSubmit>
</DrilldownManagerFooter>
)}
</>
);
};

View file

@ -0,0 +1,8 @@
/*
* 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 './create_drilldown_form';

View file

@ -0,0 +1,53 @@
/*
* 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 { EuiCallOut, EuiSpacer, EuiLink } from '@elastic/eui';
import * as React from 'react';
import { i18n } from '@kbn/i18n';
const txtDismiss = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.dismiss',
{
defaultMessage: 'Dismiss',
description: 'Dismiss button in cloning notification callout.',
}
);
const txtBody = (count: number) =>
i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.body',
{
defaultMessage: '{count, number} {count, plural, one {drilldown} other {drilldowns}} copied.',
description: 'Title of notification show when one or more drilldowns were copied.',
values: {
count,
},
}
);
export interface CloningNotificationProps {
count?: number;
}
export const CloningNotification: React.FC<CloningNotificationProps> = ({ count = 1 }) => {
const [dismissed, setDismissed] = React.useState(false);
if (dismissed) return null;
const title = (
<>
{txtBody(count)} <EuiLink onClick={() => setDismissed(true)}>{txtDismiss}</EuiLink>
</>
);
return (
<>
<EuiCallOut title={title} color="success" size="s" iconType="check" />
<EuiSpacer />
</>
);
};

View file

@ -0,0 +1,47 @@
/*
* 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 * as React from 'react';
import { DrilldownTable } from '../../components/drilldown_table';
import { useDrilldownManager } from '../context';
import { CloningNotification } from './cloning_notification';
const FIVE_SECONDS = 5e3;
export const DrilldownList: React.FC = ({}) => {
const drilldowns = useDrilldownManager();
const events = drilldowns.useEvents();
const cloningNotificationCount = React.useMemo<number>(
() =>
!!drilldowns.lastCloneRecord && drilldowns.lastCloneRecord.time > Date.now() - FIVE_SECONDS
? drilldowns.lastCloneRecord.templateIds.length
: 0,
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
React.useEffect(() => {
drilldowns.lastCloneRecord = null;
});
const notification = !!cloningNotificationCount && (
<CloningNotification count={cloningNotificationCount} />
);
return (
<>
{notification}
<DrilldownTable
items={events}
onDelete={drilldowns.onDelete}
onEdit={(id) => {
drilldowns.setRoute(['manage', id]);
}}
onCopy={drilldowns.onCreateFromDrilldown}
/>
</>
);
};

View file

@ -0,0 +1,8 @@
/*
* 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 './drilldown_list';

View file

@ -0,0 +1,31 @@
/*
* 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 * as React from 'react';
import { DrilldownManagerDependencies, PublicDrilldownManagerProps } from '../../types';
import { DrilldownManagerProvider } from '../context';
import { DrilldownManager } from './drilldown_manager';
export type PublicDrilldownManagerComponent = React.FC<PublicDrilldownManagerProps>;
/**
* This HOC creates a "public" `<DrilldownManager>` component `PublicDrilldownManagerComponent`,
* which can be exported from plugin contract for other plugins to consume.
*/
export const createPublicDrilldownManager = (
dependencies: DrilldownManagerDependencies
): PublicDrilldownManagerComponent => {
const PublicDrilldownManager: PublicDrilldownManagerComponent = (drilldownManagerProps) => {
return (
<DrilldownManagerProvider {...dependencies} {...drilldownManagerProps}>
<DrilldownManager />
</DrilldownManagerProvider>
);
};
return PublicDrilldownManager;
};

View file

@ -0,0 +1,34 @@
/*
* 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 * as React from 'react';
import { useDrilldownManager } from '../context';
import { FlyoutFrame } from '../../components/flyout_frame';
import { DrilldownManagerContent } from './drilldown_manager_content';
import { RenderDrilldownManagerTitle } from '../drilldown_manager_title';
import { RenderDrilldownManagerFooter } from '../drilldown_manager_footer';
import { HelloBar } from '../hello_bar';
export const DrilldownManager: React.FC = ({}) => {
const drilldowns = useDrilldownManager();
const route = drilldowns.useRoute();
const handleBack =
route.length < 2 ? undefined : () => drilldowns.setRoute(route.slice(0, route.length - 1));
return (
<FlyoutFrame
title={<RenderDrilldownManagerTitle />}
banner={<HelloBar />}
footer={<RenderDrilldownManagerFooter />}
onClose={drilldowns.close}
onBack={handleBack}
>
<DrilldownManagerContent />
</FlyoutFrame>
);
};

View file

@ -0,0 +1,22 @@
/*
* 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 * as React from 'react';
import { CreateDrilldownForm } from '../create_drilldown_form';
import { Tabs } from '../tabs';
import { useDrilldownManager } from '../context';
import { EditDrilldownForm } from '../edit_drilldown_form';
export const DrilldownManagerContent: React.FC = ({}) => {
const drilldowns = useDrilldownManager();
const route = drilldowns.useRoute();
if (route[0] === 'new' && !!route[1]) return <CreateDrilldownForm />;
if (route[0] === 'manage' && !!route[1]) return <EditDrilldownForm eventId={route[1]} />;
return <Tabs />;
};

View file

@ -5,4 +5,5 @@
* 2.0.
*/
export { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns';
export * from './drilldown_manager';
export * from './create_public_drilldown_manager';

View file

@ -0,0 +1,26 @@
/*
* 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 * as React from 'react';
import { useDrilldownManager } from '../context';
export const DrilldownManagerFooter: React.FC = ({ children }) => {
const drilldowns = useDrilldownManager();
React.useEffect(() => {
drilldowns.setFooter(children);
return () => {
drilldowns.setFooter(null);
};
});
return null;
};
export const RenderDrilldownManagerFooter: React.FC = () => {
const drilldowns = useDrilldownManager();
const footer = drilldowns.useFooter();
return <>{footer}</>;
};

View file

@ -0,0 +1,8 @@
/*
* 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 './drilldown_manager_footer';

View file

@ -0,0 +1,26 @@
/*
* 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 * as React from 'react';
import { useDrilldownManager } from '../context';
export const DrilldownManagerTitle: React.FC = ({ children }) => {
const drilldowns = useDrilldownManager();
React.useEffect(() => {
drilldowns.setTitle(children);
return () => {
drilldowns.resetTitle();
};
});
return null;
};
export const RenderDrilldownManagerTitle: React.FC = () => {
const drilldowns = useDrilldownManager();
const title = drilldowns.useTitle();
return <>{title}</>;
};

View file

@ -0,0 +1,8 @@
/*
* 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 './drilldown_manager_title';

View file

@ -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 React from 'react';
import { useDrilldownManager } from '../context';
import { DrilldownForm } from '../../components/drilldown_form';
import type { DrilldownState } from '../../state';
import type { TriggerPickerProps } from '../../components/trigger_picker';
export interface DrilldownStateFormProps {
state: DrilldownState;
disabled?: boolean;
}
export const DrilldownStateForm: React.FC<DrilldownStateFormProps> = ({ state, disabled }) => {
const drilldowns = useDrilldownManager();
const name = state.useName();
const triggers = state.useTriggers();
const config = state.useConfig();
const triggerPickerProps: TriggerPickerProps = React.useMemo(
() => ({
items: state.uiTriggers.map((id) => {
const trigger = drilldowns.deps.getTrigger(id);
return trigger;
}),
selected: triggers,
onChange: state.setTriggers,
}),
[drilldowns, triggers, state]
);
const context = state.getFactoryContext();
return (
<DrilldownForm
name={name}
onNameChange={state.setName}
triggers={triggerPickerProps}
disabled={disabled}
>
<state.factory.ReactCollectConfig
config={config}
onConfig={disabled ? () => {} : state.setConfig}
context={context}
/>
</DrilldownForm>
);
};

View file

@ -0,0 +1,8 @@
/*
* 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 './drilldown_state_form';

View file

@ -0,0 +1,75 @@
/*
* 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 * as React from 'react';
import { i18n } from '@kbn/i18n';
import useMountedState from 'react-use/lib/useMountedState';
import { DrilldownManagerTitle } from '../drilldown_manager_title';
import { useDrilldownManager } from '../context';
import { ActionFactoryView } from '../action_factory_view';
import { DrilldownManagerFooter } from '../drilldown_manager_footer';
import { DrilldownStateForm } from '../drilldown_state_form';
import { ButtonSubmit } from '../../components/button_submit';
const txtEditDrilldown = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.containers.editDrilldownForm.title',
{
defaultMessage: 'Edit Drilldown',
description: 'Drilldowns flyout title for edit drilldown form.',
}
);
const txtEditDrilldownButton = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.containers.editDrilldownForm.primaryButton',
{
defaultMessage: 'Save',
description: 'Primary button on new drilldown edit form.',
}
);
export interface EditDrilldownFormProps {
eventId: string;
}
export const EditDrilldownForm: React.FC<EditDrilldownFormProps> = ({ eventId }) => {
const isMounted = useMountedState();
const drilldowns = useDrilldownManager();
const drilldownState = React.useMemo(() => drilldowns.createEventDrilldownState(eventId), [
drilldowns,
eventId,
]);
const [disabled, setDisabled] = React.useState(false);
if (!drilldownState) return null;
const handleSave = () => {
setDisabled(true);
drilldowns.updateEvent(eventId, drilldownState).finally(() => {
if (!isMounted()) return;
setDisabled(false);
});
};
return (
<>
<DrilldownManagerTitle>{txtEditDrilldown}</DrilldownManagerTitle>
<ActionFactoryView
constant
factory={drilldownState.factory}
context={drilldownState.getFactoryContext()}
/>
{!!drilldownState && <DrilldownStateForm state={drilldownState} disabled={disabled} />}
{!!drilldownState && (
<DrilldownManagerFooter>
<ButtonSubmit disabled={disabled} onClick={handleSave}>
{txtEditDrilldownButton}
</ButtonSubmit>
</DrilldownManagerFooter>
)}
</>
);
};

View file

@ -0,0 +1,8 @@
/*
* 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 './edit_drilldown_form';

View file

@ -0,0 +1,45 @@
/*
* 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 { useDrilldownManager } from '../context';
import { DrilldownForm } from '../../components/drilldown_form';
import { DrilldownState } from '../../state';
import { TriggerPickerProps } from '../../components/trigger_picker';
export interface CreateDrilldownFormProps {
state: DrilldownState;
}
export const CreateDrilldownForm: React.FC<CreateDrilldownFormProps> = ({ state }) => {
const drilldowns = useDrilldownManager();
const name = state.useName();
const triggers = state.useTriggers();
const config = state.useConfig();
const triggerPickerProps: TriggerPickerProps = React.useMemo(
() => ({
items: state.uiTriggers.map((id) => {
const trigger = drilldowns.deps.getTrigger(id);
return trigger;
}),
selected: triggers,
onChange: state.setTriggers,
}),
[drilldowns, triggers, state]
);
const context = state.getFactoryContext();
return (
<DrilldownForm name={name} onNameChange={state.setName} triggers={triggerPickerProps}>
<state.factory.ReactCollectConfig
config={config}
onConfig={state.setConfig}
context={context}
/>
</DrilldownForm>
);
};

View file

@ -0,0 +1,65 @@
/*
* 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 { EuiButton, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useDrilldownManager } from '../context';
import { DrilldownForm } from '../../components/drilldown_form';
import { DrilldownState } from '../../state';
import { TriggerPickerProps } from '../../components/trigger_picker';
export const txtDeleteDrilldownButtonLabel = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel',
{
defaultMessage: 'Delete drilldown',
}
);
export interface EditDrilldownFormProps {
state: DrilldownState;
}
export const EditDrilldownForm: React.FC<EditDrilldownFormProps> = ({ state }) => {
const drilldowns = useDrilldownManager();
const name = state.useName();
const triggers = state.useTriggers();
const config = state.useConfig();
const triggerPickerProps: TriggerPickerProps = React.useMemo(
() => ({
items: state.uiTriggers.map((id) => {
const trigger = drilldowns.deps.getTrigger(id);
return trigger;
}),
selected: triggers,
onChange: state.setTriggers,
}),
[drilldowns, triggers, state]
);
const context = state.getFactoryContext();
return (
<>
<DrilldownForm name={name} onNameChange={state.setName} triggers={triggerPickerProps}>
<state.factory.ReactCollectConfig
config={config}
onConfig={state.setConfig}
context={context}
/>
</DrilldownForm>
<EuiSpacer size={'xl'} />
<EuiButton
onClick={() => {
alert('DELETE!');
}}
color={'danger'}
>
{txtDeleteDrilldownButtonLabel}
</EuiButton>
</>
);
};

View file

@ -0,0 +1,29 @@
/*
* 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 { ActionFactoryPicker } from '../action_factory_picker';
import { useDrilldownManager } from '../context';
import { CreateDrilldownForm } from './create_drilldown_form';
export const FormDrilldownWizard: React.FC = ({}) => {
const drilldowns = useDrilldownManager();
const actionFactory = drilldowns.useActionFactory();
const drilldownState = drilldowns.getDrilldownState();
let content: React.ReactNode = null;
if (!actionFactory) content = null;
if (drilldownState) content = <CreateDrilldownForm state={drilldownState} />;
return (
<>
<ActionFactoryPicker />
{content}
</>
);
};

Some files were not shown because too many files have changed in this diff Show more