mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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:
parent
60ee22361f
commit
77fe59d58f
131 changed files with 3933 additions and 1884 deletions
|
@ -7,7 +7,7 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
protected readonly children: {
|
||||
readonly children: {
|
||||
[key: string]: IEmbeddable<any, any> | ErrorEmbeddable;
|
||||
};
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
} = {};
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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()}
|
||||
/>
|
||||
),
|
||||
{
|
||||
|
|
|
@ -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()}
|
||||
/>
|
||||
),
|
||||
{
|
||||
|
|
|
@ -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はパーセントエンコーディングを使用してエスケープされます",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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} />;
|
||||
};
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './flyout_drilldown_wizard';
|
||||
export * from './action_factory_picker';
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './flyout_list_manage_drilldowns';
|
||||
export * from './presentable_picker';
|
|
@ -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')}
|
||||
/>
|
||||
));
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
.auaPresentablePicker__item {
|
||||
.euiKeyPadMenuItem__label {
|
||||
height: #{$euiSizeXL};
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
));
|
|
@ -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']);
|
||||
});
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
);
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
));
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 />);
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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' }],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
));
|
|
@ -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'>;
|
|
@ -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.
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './connected_flyout_manage_drilldowns';
|
||||
export * from './action_factory';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './list_manage_drilldowns';
|
||||
export * from './drilldown_form';
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
));
|
|
@ -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();
|
||||
});
|
|
@ -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({
|
|||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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';
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
});
|
|
@ -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';
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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 />;
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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'} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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} />;
|
||||
};
|
|
@ -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';
|
|
@ -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>;
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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;
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 />;
|
||||
};
|
|
@ -5,4 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns';
|
||||
export * from './drilldown_manager';
|
||||
export * from './create_public_drilldown_manager';
|
|
@ -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}</>;
|
||||
};
|
|
@ -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';
|
|
@ -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}</>;
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue