diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md new file mode 100644 index 0000000000000..66d540c48c3bc --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [hierarchical](./kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md) + +## AggConfigs.hierarchical property + +Signature: + +```typescript +hierarchical?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md index 22f8994747aa2..02e9a63d95ba3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md @@ -22,6 +22,7 @@ export declare class AggConfigs | --- | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.aggconfigs.aggs.md) | | IAggConfig[] | | | [createAggConfig](./kibana-plugin-plugins-data-public.aggconfigs.createaggconfig.md) | | <T extends AggConfig = AggConfig>(params: CreateAggConfigParams, { addToAggConfigs }?: {
addToAggConfigs?: boolean | undefined;
}) => T | | +| [hierarchical](./kibana-plugin-plugins-data-public.aggconfigs.hierarchical.md) | | boolean | | | [indexPattern](./kibana-plugin-plugins-data-public.aggconfigs.indexpattern.md) | | IndexPattern | | | [timeFields](./kibana-plugin-plugins-data-public.aggconfigs.timefields.md) | | string[] | | | [timeRange](./kibana-plugin-plugins-data-public.aggconfigs.timerange.md) | | TimeRange | | @@ -46,5 +47,5 @@ export declare class AggConfigs | [onSearchRequestStart(searchSource, options)](./kibana-plugin-plugins-data-public.aggconfigs.onsearchrequeststart.md) | | | | [setTimeFields(timeFields)](./kibana-plugin-plugins-data-public.aggconfigs.settimefields.md) | | | | [setTimeRange(timeRange)](./kibana-plugin-plugins-data-public.aggconfigs.settimerange.md) | | | -| [toDsl(hierarchical)](./kibana-plugin-plugins-data-public.aggconfigs.todsl.md) | | | +| [toDsl()](./kibana-plugin-plugins-data-public.aggconfigs.todsl.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.todsl.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.todsl.md index 055c4113ca3e4..1327e976db0ce 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.todsl.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.todsl.md @@ -7,15 +7,8 @@ Signature: ```typescript -toDsl(hierarchical?: boolean): Record; +toDsl(): Record; ``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| hierarchical | boolean | | - Returns: `Record` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.inspector.md similarity index 52% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.inspector.md index b4431b9467b71..9961292aaf217 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.inspector.md @@ -1,11 +1,13 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [requestResponder](./kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [inspector](./kibana-plugin-plugins-data-public.isearchoptions.inspector.md) -## ISearchOptions.requestResponder property +## ISearchOptions.inspector property + +Inspector integration options Signature: ```typescript -requestResponder?: RequestResponder; +inspector?: IInspectorInfo; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md index cc0cb538be611..21fb7e3dfc7e8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md @@ -16,10 +16,10 @@ export interface ISearchOptions | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | | [indexPattern](./kibana-plugin-plugins-data-public.isearchoptions.indexpattern.md) | IndexPattern | Index pattern reference is used for better error messages | +| [inspector](./kibana-plugin-plugins-data-public.isearchoptions.inspector.md) | IInspectorInfo | Inspector integration options | | [isRestore](./kibana-plugin-plugins-data-public.isearchoptions.isrestore.md) | boolean | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) | | [isStored](./kibana-plugin-plugins-data-public.isearchoptions.isstored.md) | boolean | Whether the session is already saved (i.e. sent to background) | | [legacyHitsTotal](./kibana-plugin-plugins-data-public.isearchoptions.legacyhitstotal.md) | boolean | Request the legacy format for the total number of hits. If sending rest_total_hits_as_int to something other than true, this should be set to false. | -| [requestResponder](./kibana-plugin-plugins-data-public.isearchoptions.requestresponder.md) | RequestResponder | | | [sessionId](./kibana-plugin-plugins-data-public.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-public.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md index 623d6366d4d13..e6ba1a51a867d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md @@ -14,7 +14,7 @@ Fetch this source and reject the returned Promise on error Signature: ```typescript -fetch(options?: ISearchOptions): Promise>; +fetch(options?: ISearchOptions): Promise>; ``` ## Parameters @@ -25,5 +25,5 @@ fetch(options?: ISearchOptions): PromiseReturns: -`Promise>` +`Promise>` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md index d5641107a88aa..4369cf7c087da 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch_.md @@ -9,7 +9,7 @@ Fetch this source from Elasticsearch, returning an observable over the response( Signature: ```typescript -fetch$(options?: ISearchOptions): import("rxjs").Observable>; +fetch$(options?: ISearchOptions): Observable>; ``` ## Parameters @@ -20,5 +20,5 @@ fetch$(options?: ISearchOptions): import("rxjs").ObservableReturns: -`import("rxjs").Observable>` +`Observable>` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md index f6bab8e424857..12011f8242996 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md @@ -9,5 +9,5 @@ Signature: ```typescript -aggs?: any; +aggs?: object | IAggConfigs | (() => object); ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md index d0f53936eb56a..981d956a9e89b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md @@ -16,7 +16,7 @@ export interface SearchSourceFields | Property | Type | Description | | --- | --- | --- | -| [aggs](./kibana-plugin-plugins-data-public.searchsourcefields.aggs.md) | any | [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) | +| [aggs](./kibana-plugin-plugins-data-public.searchsourcefields.aggs.md) | object | IAggConfigs | (() => object) | [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) | | [fields](./kibana-plugin-plugins-data-public.searchsourcefields.fields.md) | SearchFieldValue[] | Retrieve fields via the search Fields API | | [fieldsFromSource](./kibana-plugin-plugins-data-public.searchsourcefields.fieldsfromsource.md) | NameList | Retreive fields directly from \_source (legacy behavior) | | [filter](./kibana-plugin-plugins-data-public.searchsourcefields.filter.md) | Filter[] | Filter | (() => Filter[] | Filter | undefined) | [Filter](./kibana-plugin-plugins-data-public.filter.md) | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.inspector.md similarity index 52% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.inspector.md index 7440f5a9d26cf..ab755334643aa 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.inspector.md @@ -1,11 +1,13 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [requestResponder](./kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [inspector](./kibana-plugin-plugins-data-server.isearchoptions.inspector.md) -## ISearchOptions.requestResponder property +## ISearchOptions.inspector property + +Inspector integration options Signature: ```typescript -requestResponder?: RequestResponder; +inspector?: IInspectorInfo; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md index 413a59be3d427..cdb5664f96cdd 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md @@ -16,10 +16,10 @@ export interface ISearchOptions | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | | [indexPattern](./kibana-plugin-plugins-data-server.isearchoptions.indexpattern.md) | IndexPattern | Index pattern reference is used for better error messages | +| [inspector](./kibana-plugin-plugins-data-server.isearchoptions.inspector.md) | IInspectorInfo | Inspector integration options | | [isRestore](./kibana-plugin-plugins-data-server.isearchoptions.isrestore.md) | boolean | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) | | [isStored](./kibana-plugin-plugins-data-server.isearchoptions.isstored.md) | boolean | Whether the session is already saved (i.e. sent to background) | | [legacyHitsTotal](./kibana-plugin-plugins-data-server.isearchoptions.legacyhitstotal.md) | boolean | Request the legacy format for the total number of hits. If sending rest_total_hits_as_int to something other than true, this should be set to false. | -| [requestResponder](./kibana-plugin-plugins-data-server.isearchoptions.requestresponder.md) | RequestResponder | | | [sessionId](./kibana-plugin-plugins-data-server.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-server.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.getdescription.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.getdescription.md new file mode 100644 index 0000000000000..1699351349bf8 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.getdescription.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [EmbeddableFactory](./kibana-plugin-plugins-embeddable-public.embeddablefactory.md) > [getDescription](./kibana-plugin-plugins-embeddable-public.embeddablefactory.getdescription.md) + +## EmbeddableFactory.getDescription() method + +Returns a description about the embeddable. + +Signature: + +```typescript +getDescription(): string; +``` +Returns: + +`string` + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.geticontype.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.geticontype.md new file mode 100644 index 0000000000000..58b987e5630c4 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.geticontype.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [EmbeddableFactory](./kibana-plugin-plugins-embeddable-public.embeddablefactory.md) > [getIconType](./kibana-plugin-plugins-embeddable-public.embeddablefactory.geticontype.md) + +## EmbeddableFactory.getIconType() method + +Returns an EUI Icon type to be displayed in a menu. + +Signature: + +```typescript +getIconType(): string; +``` +Returns: + +`string` + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.grouping.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.grouping.md new file mode 100644 index 0000000000000..c4dbe739ddfcb --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.grouping.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [EmbeddableFactory](./kibana-plugin-plugins-embeddable-public.embeddablefactory.md) > [grouping](./kibana-plugin-plugins-embeddable-public.embeddablefactory.grouping.md) + +## EmbeddableFactory.grouping property + +Indicates the grouping this factory should appear in a sub-menu. Example, this is used for grouping options in the editors menu in Dashboard for creating new embeddables + +Signature: + +```typescript +readonly grouping?: UiActionsPresentableGrouping; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.md index b355acd0567a8..8ee60e1f58a2b 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactory.md @@ -16,6 +16,7 @@ export interface EmbeddableFactoryUiActionsPresentableGrouping | Indicates the grouping this factory should appear in a sub-menu. Example, this is used for grouping options in the editors menu in Dashboard for creating new embeddables | | [isContainerType](./kibana-plugin-plugins-embeddable-public.embeddablefactory.iscontainertype.md) | boolean | True if is this factory create embeddables that are Containers. Used in the add panel to conditionally show whether these can be added to another container. It's just not supported right now, but once nested containers are officially supported we can probably get rid of this interface. | | [isEditable](./kibana-plugin-plugins-embeddable-public.embeddablefactory.iseditable.md) | () => Promise<boolean> | Returns whether the current user should be allowed to edit this type of embeddable. Most of the time this should be based off the capabilities service, hence it's async. | | [savedObjectMetaData](./kibana-plugin-plugins-embeddable-public.embeddablefactory.savedobjectmetadata.md) | SavedObjectMetaData<TSavedObjectAttributes> | | @@ -29,6 +30,8 @@ export interface EmbeddableFactoryThis will likely change in future iterations when we improve in place editing capabilities. | | [createFromSavedObject(savedObjectId, input, parent)](./kibana-plugin-plugins-embeddable-public.embeddablefactory.createfromsavedobject.md) | Creates a new embeddable instance based off the saved object id. | | [getDefaultInput(partial)](./kibana-plugin-plugins-embeddable-public.embeddablefactory.getdefaultinput.md) | Can be used to get any default input, to be passed in to during the creation process. Default input will not be stored in a parent container, so any inherited input from a container will trump default input parameters. | +| [getDescription()](./kibana-plugin-plugins-embeddable-public.embeddablefactory.getdescription.md) | Returns a description about the embeddable. | | [getDisplayName()](./kibana-plugin-plugins-embeddable-public.embeddablefactory.getdisplayname.md) | Returns a display name for this type of embeddable. Used in "Create new... " options in the add panel for containers. | | [getExplicitInput()](./kibana-plugin-plugins-embeddable-public.embeddablefactory.getexplicitinput.md) | Can be used to request explicit input from the user, to be passed in to EmbeddableFactory:create. Explicit input is stored on the parent container for this embeddable. It overrides any inherited input passed down from the parent container. | +| [getIconType()](./kibana-plugin-plugins-embeddable-public.embeddablefactory.geticontype.md) | Returns an EUI Icon type to be displayed in a menu. | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactorydefinition.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactorydefinition.md index 6ecb88e7c017e..dd61272625160 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactorydefinition.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablefactorydefinition.md @@ -7,5 +7,5 @@ Signature: ```typescript -export declare type EmbeddableFactoryDefinition = IEmbeddable, T extends SavedObjectAttributes = SavedObjectAttributes> = Pick, 'create' | 'type' | 'isEditable' | 'getDisplayName'> & Partial, 'createFromSavedObject' | 'isContainerType' | 'getExplicitInput' | 'savedObjectMetaData' | 'canCreateNew' | 'getDefaultInput' | 'telemetry' | 'extract' | 'inject' | 'migrations'>>; +export declare type EmbeddableFactoryDefinition = IEmbeddable, T extends SavedObjectAttributes = SavedObjectAttributes> = Pick, 'create' | 'type' | 'isEditable' | 'getDisplayName'> & Partial, 'createFromSavedObject' | 'isContainerType' | 'getExplicitInput' | 'savedObjectMetaData' | 'canCreateNew' | 'getDefaultInput' | 'telemetry' | 'extract' | 'inject' | 'migrations' | 'grouping' | 'getIconType' | 'getDescription'>>; ``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md index add4646375359..90caaa3035b34 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md @@ -14,6 +14,7 @@ export declare function openAddPanelFlyout(options: { overlays: OverlayStart; notifications: NotificationsStart; SavedObjectFinder: React.ComponentType; + showCreateNewMenu?: boolean; }): OverlayRef; ``` @@ -21,7 +22,7 @@ export declare function openAddPanelFlyout(options: { | Parameter | Type | Description | | --- | --- | --- | -| options | {
embeddable: IContainer;
getFactory: EmbeddableStart['getEmbeddableFactory'];
getAllFactories: EmbeddableStart['getEmbeddableFactories'];
overlays: OverlayStart;
notifications: NotificationsStart;
SavedObjectFinder: React.ComponentType<any>;
} | | +| options | {
embeddable: IContainer;
getFactory: EmbeddableStart['getEmbeddableFactory'];
getAllFactories: EmbeddableStart['getEmbeddableFactories'];
overlays: OverlayStart;
notifications: NotificationsStart;
SavedObjectFinder: React.ComponentType<any>;
showCreateNewMenu?: boolean;
} | | Returns: diff --git a/examples/search_examples/public/index.scss b/examples/search_examples/public/index.scss index e69de29bb2d1d..b623fecf78640 100644 --- a/examples/search_examples/public/index.scss +++ b/examples/search_examples/public/index.scss @@ -0,0 +1,6 @@ +@import '@elastic/eui/src/global_styling/variables/header'; + +.searchExampleStepDsc { + padding-left: $euiSizeXL; + font-style: italic; +} diff --git a/examples/search_examples/public/search/app.tsx b/examples/search_examples/public/search/app.tsx index 8f31d242faf5e..b2a4991d0717b 100644 --- a/examples/search_examples/public/search/app.tsx +++ b/examples/search_examples/public/search/app.tsx @@ -20,13 +20,13 @@ import { EuiTitle, EuiText, EuiFlexGrid, - EuiFlexGroup, EuiFlexItem, EuiCheckbox, EuiSpacer, EuiCode, EuiComboBox, EuiFormLabel, + EuiTabbedContent, } from '@elastic/eui'; import { CoreStart } from '../../../../src/core/public'; @@ -60,6 +60,11 @@ function getNumeric(fields?: IndexPatternField[]) { return fields?.filter((f) => f.type === 'number' && f.aggregatable); } +function getAggregatableStrings(fields?: IndexPatternField[]) { + if (!fields) return []; + return fields?.filter((f) => f.type === 'string' && f.aggregatable); +} + function formatFieldToComboBox(field?: IndexPatternField | null) { if (!field) return []; return formatFieldsToComboBox([field]); @@ -90,6 +95,9 @@ export const SearchExamplesApp = ({ const [selectedNumericField, setSelectedNumericField] = useState< IndexPatternField | null | undefined >(); + const [selectedBucketField, setSelectedBucketField] = useState< + IndexPatternField | null | undefined + >(); const [request, setRequest] = useState>({}); const [response, setResponse] = useState>({}); @@ -108,6 +116,7 @@ export const SearchExamplesApp = ({ setFields(indexPattern?.fields); }, [indexPattern]); useEffect(() => { + setSelectedBucketField(fields?.length ? getAggregatableStrings(fields)[0] : null); setSelectedNumericField(fields?.length ? getNumeric(fields)[0] : null); }, [fields]); @@ -203,28 +212,40 @@ export const SearchExamplesApp = ({ .setField('index', indexPattern) .setField('filter', filters) .setField('query', query) - .setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : ['*']) + .setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : ['']) + .setField('size', selectedFields.length ? 100 : 0) .setField('trackTotalHits', 100); - if (selectedNumericField) { - searchSource.setField('aggs', () => { - return data.search.aggs - .createAggConfigs(indexPattern, [ - { type: 'avg', params: { field: selectedNumericField.name } }, - ]) - .toDsl(); + const aggDef = []; + if (selectedBucketField) { + aggDef.push({ + type: 'terms', + schema: 'split', + params: { field: selectedBucketField.name, size: 2, otherBucket: true }, }); } + if (selectedNumericField) { + aggDef.push({ type: 'avg', params: { field: selectedNumericField.name } }); + } + if (aggDef.length > 0) { + const ac = data.search.aggs.createAggConfigs(indexPattern, aggDef); + searchSource.setField('aggs', ac); + } setRequest(searchSource.getSearchRequestBody()); const res = await searchSource.fetch$().toPromise(); setResponse(res); const message = Searched {res.hits.total} documents.; - notifications.toasts.addSuccess({ - title: 'Query result', - text: mountReactNode(message), - }); + notifications.toasts.addSuccess( + { + title: 'Query result', + text: mountReactNode(message), + }, + { + toastLifeTimeMs: 300000, + } + ); } catch (e) { setResponse(e.body); notifications.toasts.addWarning(`An error has occurred: ${e.message}`); @@ -263,6 +284,55 @@ export const SearchExamplesApp = ({ doSearchSourceSearch(); }; + const reqTabs = [ + { + id: 'request', + name: Request, + content: ( + <> + + Search body sent to ES + + {JSON.stringify(request, null, 2)} + + + ), + }, + { + id: 'response', + name: Response, + content: ( + <> + + + + + + {JSON.stringify(response, null, 2)} + + + ), + }, + ]; + return ( @@ -284,59 +354,75 @@ export const SearchExamplesApp = ({ useDefaultBehaviors={true} indexPatterns={indexPattern ? [indexPattern] : undefined} /> - + + + Index Pattern + { + const newIndexPattern = await data.indexPatterns.get(newIndexPatternId); + setIndexPattern(newIndexPattern); + }} + isClearable={false} + /> + + + Field (bucket) + { + if (option.length) { + const fld = indexPattern?.getFieldByName(option[0].label); + setSelectedBucketField(fld || null); + } else { + setSelectedBucketField(null); + } + }} + sortMatchesBy="startsWith" + data-test-subj="searchBucketField" + /> + + + Numeric Field (metric) + { + if (option.length) { + const fld = indexPattern?.getFieldByName(option[0].label); + setSelectedNumericField(fld || null); + } else { + setSelectedNumericField(null); + } + }} + sortMatchesBy="startsWith" + data-test-subj="searchMetricField" + /> + + + Fields to queryString + { + const flds = option + .map((opt) => indexPattern?.getFieldByName(opt?.label)) + .filter((f) => f); + setSelectedFields(flds.length ? (flds as IndexPatternField[]) : []); + }} + sortMatchesBy="startsWith" + /> + + + - - - - Index Pattern - { - const newIndexPattern = await data.indexPatterns.get(newIndexPatternId); - setIndexPattern(newIndexPattern); - }} - isClearable={false} - /> - - - Numeric Field to Aggregate - { - const fld = indexPattern?.getFieldByName(option[0].label); - setSelectedNumericField(fld || null); - }} - sortMatchesBy="startsWith" - /> - - - - - Fields to query (leave blank to include all fields) - { - const flds = option - .map((opt) => indexPattern?.getFieldByName(opt?.label)) - .filter((f) => f); - setSelectedFields(flds.length ? (flds as IndexPatternField[]) : []); - }} - sortMatchesBy="startsWith" - /> - - -

@@ -352,15 +438,32 @@ export const SearchExamplesApp = ({ - + + + + + + + @@ -446,41 +549,8 @@ export const SearchExamplesApp = ({ - - -

Request

-
- Search body sent to ES - - {JSON.stringify(request, null, 2)} - -
- - -

Response

-
- - - - - {JSON.stringify(response, null, 2)} - + + diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 41335069461fa..54eaf461b73d7 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -11,7 +11,8 @@ "share", "uiActions", "urlForwarding", - "presentationUtil" + "presentationUtil", + "visualizations" ], "optionalPlugins": [ "home", diff --git a/src/plugins/dashboard/public/application/_dashboard_app.scss b/src/plugins/dashboard/public/application/_dashboard_app.scss index 30253afff391f..f6525377cce70 100644 --- a/src/plugins/dashboard/public/application/_dashboard_app.scss +++ b/src/plugins/dashboard/public/application/_dashboard_app.scss @@ -66,4 +66,17 @@ .dshUnsavedListingItem__actions { flex-direction: column; } -} \ No newline at end of file +} + +// Temporary fix for two tone icons to make them monochrome +.dshSolutionToolbar__editorContextMenu--dark { + .euiIcon path { + fill: $euiColorGhost; + } +} + +.dshSolutionToolbar__editorContextMenu--light { + .euiIcon path { + fill: $euiColorInk; + } +} diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index e5281a257ee13..ed68afc5e97b1 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -80,6 +80,7 @@ export async function mountApp({ embeddable: embeddableStart, kibanaLegacy: { dashboardConfig }, savedObjectsTaggingOss, + visualizations, } = pluginsStart; const spacesApi = pluginsStart.spacesOss?.isSpacesAvailable ? pluginsStart.spacesOss : undefined; @@ -123,6 +124,7 @@ export async function mountApp({ visualizeCapabilities: { save: Boolean(coreStart.application.capabilities.visualize?.save) }, storeSearchSession: Boolean(coreStart.application.capabilities.dashboard.storeSearchSession), }, + visualizations, }; const getUrlStateStorage = (history: RouteComponentProps['history']) => diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx index 9b93f0bbd0711..ff592742488f5 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx @@ -49,7 +49,7 @@ export class DashboardContainerFactoryDefinition public readonly getDisplayName = () => { return i18n.translate('dashboard.factory.displayName', { - defaultMessage: 'dashboard', + defaultMessage: 'Dashboard', }); }; diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap index 4cd3eb13f3609..138d665866af0 100644 --- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -287,7 +287,7 @@ exports[`DashboardEmptyScreen renders correctly with edit mode 1`] = `

- Add your first panel + Add your first visualization

().services; const [state, setState] = useState({ chromeIsVisible: false }); const [isSaveInProgress, setIsSaveInProgress] = useState(false); + const lensAlias = visualizations.getAliases().find(({ name }) => name === 'lens'); + const quickButtonVisTypes = ['markdown', 'maps']; const stateTransferService = embeddable.getStateTransfer(); + const IS_DARK_THEME = uiSettings.get('theme:darkMode'); + + const trackUiMetric = usageCollection?.reportUiCounter.bind( + usageCollection, + DashboardConstants.DASHBOARDS_ID + ); useEffect(() => { const visibleSubscription = chrome.getIsVisible$().subscribe((chromeIsVisible) => { @@ -152,27 +161,36 @@ export function DashboardTopNav({ uiSettings, ]); - const createNew = useCallback(async () => { - const type = 'visualization'; - const factory = embeddable.getEmbeddableFactory(type); + const createNewVisType = useCallback( + (visType?: BaseVisType | VisTypeAlias) => () => { + let path = ''; + let appId = ''; - if (!factory) { - throw new EmbeddableFactoryNotFoundError(type); - } + if (visType) { + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, visType.name); + } - await factory.create({} as EmbeddableInput, dashboardContainer); - }, [dashboardContainer, embeddable]); + if ('aliasPath' in visType) { + appId = visType.aliasApp; + path = visType.aliasPath; + } else { + appId = 'visualize'; + path = `#/create?type=${encodeURIComponent(visType.name)}`; + } + } else { + appId = 'visualize'; + path = '#/create?'; + } - const createNewVisType = useCallback( - (newVisType: string) => async () => { - stateTransferService.navigateToEditor('visualize', { - path: `#/create?type=${encodeURIComponent(newVisType)}`, + stateTransferService.navigateToEditor(appId, { + path, state: { originatingApp: DashboardConstants.DASHBOARDS_ID, }, }); }, - [stateTransferService] + [trackUiMetric, stateTransferService] ); const clearAddPanel = useCallback(() => { @@ -563,38 +581,57 @@ export function DashboardTopNav({ const { TopNavMenu } = navigation.ui; - const quickButtons = [ - { - iconType: 'visText', - createType: i18n.translate('dashboard.solutionToolbar.markdownQuickButtonLabel', { - defaultMessage: 'Markdown', - }), - onClick: createNewVisType('markdown'), - 'data-test-subj': 'dashboardMarkdownQuickButton', - }, - { - iconType: 'controlsHorizontal', - createType: i18n.translate('dashboard.solutionToolbar.inputControlsQuickButtonLabel', { - defaultMessage: 'Input control', - }), - onClick: createNewVisType('input_control_vis'), - 'data-test-subj': 'dashboardInputControlsQuickButton', - }, - ]; + const getVisTypeQuickButton = (visTypeName: string) => { + const visType = + visualizations.get(visTypeName) || + visualizations.getAliases().find(({ name }) => name === visTypeName); + + if (visType) { + if ('aliasPath' in visType) { + const { name, icon, title } = visType as VisTypeAlias; + + return { + iconType: icon, + createType: title, + onClick: createNewVisType(visType as VisTypeAlias), + 'data-test-subj': `dashboardQuickButton${name}`, + isDarkModeEnabled: IS_DARK_THEME, + }; + } else { + const { name, icon, title, titleInWizard } = visType as BaseVisType; + + return { + iconType: icon, + createType: titleInWizard || title, + onClick: createNewVisType(visType as BaseVisType), + 'data-test-subj': `dashboardQuickButton${name}`, + isDarkModeEnabled: IS_DARK_THEME, + }; + } + } + + return; + }; + + const quickButtons = quickButtonVisTypes + .map(getVisTypeQuickButton) + .filter((button) => button) as QuickButtonProps[]; return ( <> + {viewMode !== ViewMode.VIEW ? ( - + {{ primaryActionButton: ( ), @@ -605,6 +642,12 @@ export function DashboardTopNav({ data-test-subj="dashboardAddPanelButton" /> ), + extraButtons: [ + , + ], }} ) : null} diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx new file mode 100644 index 0000000000000..5205f5b294c4f --- /dev/null +++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx @@ -0,0 +1,255 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; +import { + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, + EuiContextMenuItemIcon, +} from '@elastic/eui'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { i18n } from '@kbn/i18n'; +import { BaseVisType, VisGroups, VisTypeAlias } from '../../../../visualizations/public'; +import { SolutionToolbarPopover } from '../../../../presentation_util/public'; +import { EmbeddableFactoryDefinition, EmbeddableInput } from '../../services/embeddable'; +import { useKibana } from '../../services/kibana_react'; +import { DashboardAppServices } from '../types'; +import { DashboardContainer } from '..'; +import { DashboardConstants } from '../../dashboard_constants'; +import { dashboardReplacePanelAction } from '../../dashboard_strings'; + +interface Props { + /** Dashboard container */ + dashboardContainer: DashboardContainer; + /** Handler for creating new visualization of a specified type */ + createNewVisType: (visType: BaseVisType | VisTypeAlias) => () => void; +} + +interface FactoryGroup { + id: string; + appName: string; + icon: EuiContextMenuItemIcon; + panelId: number; + factories: EmbeddableFactoryDefinition[]; +} + +export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => { + const { + core, + embeddable, + visualizations, + usageCollection, + uiSettings, + } = useKibana().services; + + const IS_DARK_THEME = uiSettings.get('theme:darkMode'); + + const trackUiMetric = usageCollection?.reportUiCounter.bind( + usageCollection, + DashboardConstants.DASHBOARDS_ID + ); + + const createNewAggsBasedVis = useCallback( + (visType?: BaseVisType) => () => + visualizations.showNewVisModal({ + originatingApp: DashboardConstants.DASHBOARDS_ID, + outsideVisualizeApp: true, + showAggsSelection: true, + selectedVisType: visType, + }), + [visualizations] + ); + + const getVisTypesByGroup = (group: VisGroups) => + visualizations + .getByGroup(group) + .sort(({ name: a }: BaseVisType | VisTypeAlias, { name: b }: BaseVisType | VisTypeAlias) => { + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }) + .filter(({ hidden }: BaseVisType) => !hidden); + + const promotedVisTypes = getVisTypesByGroup(VisGroups.PROMOTED); + const aggsBasedVisTypes = getVisTypesByGroup(VisGroups.AGGBASED); + const toolVisTypes = getVisTypesByGroup(VisGroups.TOOLS); + const visTypeAliases = visualizations + .getAliases() + .sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => + a === b ? 0 : a ? -1 : 1 + ); + + const factories = embeddable + ? Array.from(embeddable.getEmbeddableFactories()).filter( + ({ type, isEditable, canCreateNew, isContainerType }) => + isEditable() && !isContainerType && canCreateNew() && type !== 'visualization' + ) + : []; + + const factoryGroupMap: Record = {}; + const ungroupedFactories: EmbeddableFactoryDefinition[] = []; + const aggBasedPanelID = 1; + + let panelCount = 1 + aggBasedPanelID; + + factories.forEach((factory: EmbeddableFactoryDefinition, index) => { + const { grouping } = factory; + + if (grouping) { + grouping.forEach((group) => { + if (factoryGroupMap[group.id]) { + factoryGroupMap[group.id].factories.push(factory); + } else { + factoryGroupMap[group.id] = { + id: group.id, + appName: group.getDisplayName ? group.getDisplayName({ embeddable }) : group.id, + icon: (group.getIconType + ? group.getIconType({ embeddable }) + : 'empty') as EuiContextMenuItemIcon, + factories: [factory], + panelId: panelCount, + }; + + panelCount++; + } + }); + } else { + ungroupedFactories.push(factory); + } + }); + + const getVisTypeMenuItem = (visType: BaseVisType): EuiContextMenuPanelItemDescriptor => { + const { name, title, titleInWizard, description, icon = 'empty', group } = visType; + return { + name: titleInWizard || title, + icon: icon as string, + onClick: + group === VisGroups.AGGBASED ? createNewAggsBasedVis(visType) : createNewVisType(visType), + 'data-test-subj': `visType-${name}`, + toolTipContent: description, + }; + }; + + const getVisTypeAliasMenuItem = ( + visTypeAlias: VisTypeAlias + ): EuiContextMenuPanelItemDescriptor => { + const { name, title, description, icon = 'empty' } = visTypeAlias; + + return { + name: title, + icon, + onClick: createNewVisType(visTypeAlias), + 'data-test-subj': `visType-${name}`, + toolTipContent: description, + }; + }; + + const getEmbeddableFactoryMenuItem = ( + factory: EmbeddableFactoryDefinition + ): EuiContextMenuPanelItemDescriptor => { + const icon = factory?.getIconType ? factory.getIconType() : 'empty'; + + const toolTipContent = factory?.getDescription ? factory.getDescription() : undefined; + + return { + name: factory.getDisplayName(), + icon, + toolTipContent, + onClick: async () => { + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, factory.type); + } + let newEmbeddable; + if (factory.getExplicitInput) { + const explicitInput = await factory.getExplicitInput(); + newEmbeddable = await dashboardContainer.addNewEmbeddable(factory.type, explicitInput); + } else { + newEmbeddable = await factory.create({} as EmbeddableInput, dashboardContainer); + } + + if (newEmbeddable) { + core.notifications.toasts.addSuccess({ + title: dashboardReplacePanelAction.getSuccessMessage( + `'${newEmbeddable.getInput().title}'` || '' + ), + 'data-test-subj': 'addEmbeddableToDashboardSuccess', + }); + } + }, + 'data-test-subj': `createNew-${factory.type}`, + }; + }; + + const aggsPanelTitle = i18n.translate('dashboard.editorMenu.aggBasedGroupTitle', { + defaultMessage: 'Aggregation based', + }); + + const editorMenuPanels = [ + { + id: 0, + items: [ + ...visTypeAliases.map(getVisTypeAliasMenuItem), + ...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({ + name: appName, + icon, + panel: panelId, + 'data-test-subj': `dashboardEditorMenu-${id}Group`, + })), + ...ungroupedFactories.map(getEmbeddableFactoryMenuItem), + ...promotedVisTypes.map(getVisTypeMenuItem), + { + name: aggsPanelTitle, + icon: 'visualizeApp', + panel: aggBasedPanelID, + 'data-test-subj': `dashboardEditorAggBasedMenuItem`, + }, + ...toolVisTypes.map(getVisTypeMenuItem), + ], + }, + { + id: aggBasedPanelID, + title: aggsPanelTitle, + items: aggsBasedVisTypes.map(getVisTypeMenuItem), + }, + ...Object.values(factoryGroupMap).map( + ({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({ + id: panelId, + title: appName, + items: groupFactories.map(getEmbeddableFactoryMenuItem), + }) + ), + ]; + + return ( + + + + ); +}; diff --git a/src/plugins/dashboard/public/application/types.ts b/src/plugins/dashboard/public/application/types.ts index 6415fdfd73ee8..dd291291ce9d6 100644 --- a/src/plugins/dashboard/public/application/types.ts +++ b/src/plugins/dashboard/public/application/types.ts @@ -25,6 +25,7 @@ import { DataPublicPluginStart, IndexPatternsContract } from '../services/data'; import { SavedObjectLoader, SavedObjectsStart } from '../services/saved_objects'; import { DashboardPanelStorage } from './lib'; import { UrlForwardingStart } from '../../../url_forwarding/public'; +import { VisualizationsStart } from '../../../visualizations/public'; export type DashboardRedirect = (props: RedirectToProps) => void; export type RedirectToProps = @@ -83,4 +84,5 @@ export interface DashboardAppServices { savedObjectsClient: SavedObjectsClientContract; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; savedQueryService: DataPublicPluginStart['query']['savedQueries']; + visualizations: VisualizationsStart; } diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index 79a59d0cfa605..531ff815312cf 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -377,7 +377,7 @@ export const emptyScreenStrings = { }), getEmptyWidgetTitle: () => i18n.translate('dashboard.emptyWidget.addPanelTitle', { - defaultMessage: 'Add your first panel', + defaultMessage: 'Add your first visualization', }), getEmptyWidgetDescription: () => i18n.translate('dashboard.emptyWidget.addPanelDescription', { diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index e2f52a47455b3..0fad1c51f433a 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -24,6 +24,7 @@ import { PluginInitializerContext, SavedObjectsClientContract, } from '../../../core/public'; +import { VisualizationsStart } from '../../visualizations/public'; import { createKbnUrlTracker } from './services/kibana_utils'; import { UsageCollectionSetup } from './services/usage_collection'; @@ -115,6 +116,7 @@ export interface DashboardStartDependencies { presentationUtil: PresentationUtilPluginStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; spacesOss?: SpacesOssPluginStart; + visualizations: VisualizationsStart; } export type DashboardSetup = void; diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts index 3ce528e6ed893..28102544ae055 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts @@ -342,8 +342,8 @@ describe('AggConfigs', () => { { enabled: true, type: 'max', schema: 'metric', params: { field: 'bytes' } }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const topLevelDsl = ac.toDsl(true); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, hierarchical: true }); + const topLevelDsl = ac.toDsl(); const buckets = ac.bySchemaName('buckets'); const metrics = ac.bySchemaName('metrics'); @@ -412,8 +412,8 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const topLevelDsl = ac.toDsl(true)['2']; + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, hierarchical: true }); + const topLevelDsl = ac.toDsl()['2']; expect(Object.keys(topLevelDsl.aggs)).toContain('1'); expect(Object.keys(topLevelDsl.aggs)).toContain('1-bucket'); diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index 4d5d49754387d..2932ef7325aed 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -43,6 +43,7 @@ function parseParentAggs(dslLvlCursor: any, dsl: any) { export interface AggConfigsOptions { typesRegistry: AggTypesRegistryStart; + hierarchical?: boolean; } export type CreateAggConfigParams = Assign; @@ -65,6 +66,8 @@ export class AggConfigs { public indexPattern: IndexPattern; public timeRange?: TimeRange; public timeFields?: string[]; + public hierarchical?: boolean = false; + private readonly typesRegistry: AggTypesRegistryStart; aggs: IAggConfig[]; @@ -80,6 +83,7 @@ export class AggConfigs { this.aggs = []; this.indexPattern = indexPattern; + this.hierarchical = opts.hierarchical; configStates.forEach((params: any) => this.createAggConfig(params)); } @@ -174,12 +178,12 @@ export class AggConfigs { return true; } - toDsl(hierarchical: boolean = false): Record { + toDsl(): Record { const dslTopLvl = {}; let dslLvlCursor: Record; let nestedMetrics: Array<{ config: AggConfig; dsl: Record }> | []; - if (hierarchical) { + if (this.hierarchical) { // collect all metrics, and filter out the ones that we won't be copying nestedMetrics = this.aggs .filter(function (agg) { diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 33fdc45a605b7..f0f3912bf64fe 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -13,12 +13,23 @@ import { ISearchSource } from 'src/plugins/data/public'; import { DatatableColumnType, SerializedFieldFormat } from 'src/plugins/expressions/common'; import type { RequestAdapter } from 'src/plugins/inspector/common'; +import { estypes } from '@elastic/elasticsearch'; import { initParams } from './agg_params'; import { AggConfig } from './agg_config'; import { IAggConfigs } from './agg_configs'; import { BaseParamType } from './param_types/base'; import { AggParamType } from './param_types/agg'; +type PostFlightRequestFn = ( + resp: estypes.SearchResponse, + aggConfigs: IAggConfigs, + aggConfig: TAggConfig, + searchSource: ISearchSource, + inspectorRequestAdapter?: RequestAdapter, + abortSignal?: AbortSignal, + searchSessionId?: string +) => Promise>; + export interface AggTypeConfig< TAggConfig extends AggConfig = AggConfig, TParam extends AggParamType = AggParamType @@ -40,15 +51,7 @@ export interface AggTypeConfig< customLabels?: boolean; json?: boolean; decorateAggConfig?: () => any; - postFlightRequest?: ( - resp: any, - aggConfigs: IAggConfigs, - aggConfig: TAggConfig, - searchSource: ISearchSource, - inspectorRequestAdapter?: RequestAdapter, - abortSignal?: AbortSignal, - searchSessionId?: string - ) => Promise; + postFlightRequest?: PostFlightRequestFn; getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat; getValue?: (agg: TAggConfig, bucket: any) => any; getKey?: (bucket: any, key: any, agg: TAggConfig) => any; @@ -188,15 +191,7 @@ export class AggType< * @param searchSessionId - searchSessionId to be used for grouping requests into a single search session * @return {Promise} */ - postFlightRequest: ( - resp: any, - aggConfigs: IAggConfigs, - aggConfig: TAggConfig, - searchSource: ISearchSource, - inspectorRequestAdapter?: RequestAdapter, - abortSignal?: AbortSignal, - searchSessionId?: string - ) => Promise; + postFlightRequest: PostFlightRequestFn; /** * Get the serialized format for the values produced by this agg type, * overridden by several metrics that always output a simple number. diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts index 56e720d237c45..2aa0d346afe34 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts @@ -433,7 +433,7 @@ describe('Terms Agg Other bucket helper', () => { aggConfigs.aggs[0] as IBucketAggConfig, otherAggConfig() ); - expect(mergedResponse.aggregations['1'].buckets[3].key).toEqual('__other__'); + expect((mergedResponse!.aggregations!['1'] as any).buckets[3].key).toEqual('__other__'); } }); @@ -455,7 +455,7 @@ describe('Terms Agg Other bucket helper', () => { otherAggConfig() ); - expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].key).toEqual( + expect((mergedResponse!.aggregations!['1'] as any).buckets[1]['2'].buckets[3].key).toEqual( '__other__' ); } @@ -471,7 +471,7 @@ describe('Terms Agg Other bucket helper', () => { aggConfigs.aggs[0] as IBucketAggConfig ); expect( - updatedResponse.aggregations['1'].buckets.find( + (updatedResponse!.aggregations!['1'] as any).buckets.find( (bucket: Record) => bucket.key === '__missing__' ) ).toBeDefined(); diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts index 742615bc49d8f..6230ae897b170 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts @@ -7,6 +7,7 @@ */ import { isNumber, keys, values, find, each, cloneDeep, flatten } from 'lodash'; +import { estypes } from '@elastic/elasticsearch'; import { buildExistsFilter, buildPhrasesFilter, buildQueryFromFilters } from '../../../../common'; import { AggGroupNames } from '../agg_groups'; import { IAggConfigs } from '../agg_configs'; @@ -42,7 +43,7 @@ const getNestedAggDSL = (aggNestedDsl: Record, startFromAggId: stri */ const getAggResultBuckets = ( aggConfigs: IAggConfigs, - response: any, + response: estypes.SearchResponse['aggregations'], aggWithOtherBucket: IBucketAggConfig, key: string ) => { @@ -72,8 +73,8 @@ const getAggResultBuckets = ( } } } - if (responseAgg[aggWithOtherBucket.id]) { - return responseAgg[aggWithOtherBucket.id].buckets; + if (responseAgg?.[aggWithOtherBucket.id]) { + return (responseAgg[aggWithOtherBucket.id] as any).buckets; } return []; }; @@ -235,11 +236,11 @@ export const buildOtherBucketAgg = ( export const mergeOtherBucketAggResponse = ( aggsConfig: IAggConfigs, - response: any, + response: estypes.SearchResponse, otherResponse: any, otherAgg: IBucketAggConfig, requestAgg: Record -) => { +): estypes.SearchResponse => { const updatedResponse = cloneDeep(response); each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => { if (!bucket.doc_count || key === undefined) return; @@ -276,7 +277,7 @@ export const mergeOtherBucketAggResponse = ( }; export const updateMissingBucket = ( - response: any, + response: estypes.SearchResponse, aggConfigs: IAggConfigs, agg: IBucketAggConfig ) => { diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index 77c9c6e391c0a..03cf14a577a50 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -101,25 +101,21 @@ export const getTermsBucketAgg = () => nestedSearchSource.setField('aggs', filterAgg); - const requestResponder = inspectorRequestAdapter?.start( - i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', { - defaultMessage: 'Other bucket', - }), - { - description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', { - defaultMessage: - 'This request counts the number of documents that fall ' + - 'outside the criterion of the data buckets.', - }), - searchSessionId, - } - ); - const response = await nestedSearchSource .fetch$({ abortSignal, sessionId: searchSessionId, - requestResponder, + inspector: { + adapter: inspectorRequestAdapter, + title: i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', { + defaultMessage: 'Other bucket', + }), + description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', { + defaultMessage: + 'This request counts the number of documents that fall ' + + 'outside the criterion of the data buckets.', + }), + }, }) .toPromise(); diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts index c2566535916a8..b30e5740fa3fb 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts @@ -9,7 +9,7 @@ import type { MockedKeys } from '@kbn/utility-types/jest'; import type { Filter } from '../../../es_query'; import type { IndexPattern } from '../../../index_patterns'; -import type { IAggConfig, IAggConfigs } from '../../aggs'; +import type { IAggConfigs } from '../../aggs'; import type { ISearchSource } from '../../search_source'; import { searchSourceCommonMock } from '../../search_source/mocks'; @@ -38,7 +38,6 @@ describe('esaggs expression function - public', () => { filters: undefined, indexPattern: ({ id: 'logstash-*' } as unknown) as jest.Mocked, inspectorAdapters: {}, - metricsAtAllLevels: false, partialRows: false, query: undefined, searchSessionId: 'abc123', @@ -76,21 +75,7 @@ describe('esaggs expression function - public', () => { test('setField(aggs)', async () => { expect(searchSource.setField).toHaveBeenCalledTimes(5); - expect(typeof (searchSource.setField as jest.Mock).mock.calls[2][1]).toBe('function'); - expect((searchSource.setField as jest.Mock).mock.calls[2][1]()).toEqual( - mockParams.aggs.toDsl() - ); - expect(mockParams.aggs.toDsl).toHaveBeenCalledWith(mockParams.metricsAtAllLevels); - - // make sure param is passed through - jest.clearAllMocks(); - await handleRequest({ - ...mockParams, - metricsAtAllLevels: true, - }); - searchSource = await mockParams.searchSourceService.create(); - (searchSource.setField as jest.Mock).mock.calls[2][1](); - expect(mockParams.aggs.toDsl).toHaveBeenCalledWith(true); + expect((searchSource.setField as jest.Mock).mock.calls[2][1]).toEqual(mockParams.aggs); }); test('setField(filter)', async () => { @@ -133,36 +118,24 @@ describe('esaggs expression function - public', () => { test('calls searchSource.fetch', async () => { await handleRequest(mockParams); const searchSource = await mockParams.searchSourceService.create(); + expect(searchSource.fetch$).toHaveBeenCalledWith({ abortSignal: mockParams.abortSignal, sessionId: mockParams.searchSessionId, + inspector: { + title: 'Data', + description: 'This request queries Elasticsearch to fetch the data for the visualization.', + adapter: undefined, + }, }); }); - test('calls agg.postFlightRequest if it exiests and agg is enabled', async () => { - mockParams.aggs.aggs[0].enabled = true; - await handleRequest(mockParams); - expect(mockParams.aggs.aggs[0].type.postFlightRequest).toHaveBeenCalledTimes(1); - - // ensure it works if the function doesn't exist - jest.clearAllMocks(); - mockParams.aggs.aggs[0] = ({ type: { name: 'count' } } as unknown) as IAggConfig; - expect(async () => await handleRequest(mockParams)).not.toThrowError(); - }); - - test('should skip agg.postFlightRequest call if the agg is disabled', async () => { - mockParams.aggs.aggs[0].enabled = false; - await handleRequest(mockParams); - expect(mockParams.aggs.aggs[0].type.postFlightRequest).toHaveBeenCalledTimes(0); - }); - test('tabifies response data', async () => { await handleRequest(mockParams); expect(tabifyAggResponse).toHaveBeenCalledWith( mockParams.aggs, {}, { - metricsAtAllLevels: mockParams.metricsAtAllLevels, partialRows: mockParams.partialRows, timeRange: mockParams.timeRange, } diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts index 5620698a47538..173b2067cad6b 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts @@ -40,28 +40,12 @@ export interface RequestHandlerParams { getNow?: () => Date; } -function getRequestMainResponder(inspectorAdapters: Adapters, searchSessionId?: string) { - return inspectorAdapters.requests?.start( - i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { - defaultMessage: 'Data', - }), - { - description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { - defaultMessage: - 'This request queries Elasticsearch to fetch the data for the visualization.', - }), - searchSessionId, - } - ); -} - export const handleRequest = async ({ abortSignal, aggs, filters, indexPattern, inspectorAdapters, - metricsAtAllLevels, partialRows, query, searchSessionId, @@ -100,9 +84,7 @@ export const handleRequest = async ({ }, }); - requestSearchSource.setField('aggs', function () { - return aggs.toDsl(metricsAtAllLevels); - }); + requestSearchSource.setField('aggs', aggs); requestSearchSource.onRequestStart((paramSearchSource, options) => { return aggs.onSearchRequestStart(paramSearchSource, options); @@ -128,35 +110,27 @@ export const handleRequest = async ({ requestSearchSource.setField('query', query); inspectorAdapters.requests?.reset(); - const requestResponder = getRequestMainResponder(inspectorAdapters, searchSessionId); - const response$ = await requestSearchSource.fetch$({ - abortSignal, - sessionId: searchSessionId, - requestResponder, - }); - - // Note that rawResponse is not deeply cloned here, so downstream applications using courier - // must take care not to mutate it, or it could have unintended side effects, e.g. displaying - // response data incorrectly in the inspector. - let response = await response$.toPromise(); - for (const agg of aggs.aggs) { - if (agg.enabled && typeof agg.type.postFlightRequest === 'function') { - response = await agg.type.postFlightRequest( - response, - aggs, - agg, - requestSearchSource, - inspectorAdapters.requests, - abortSignal, - searchSessionId - ); - } - } + const response = await requestSearchSource + .fetch$({ + abortSignal, + sessionId: searchSessionId, + inspector: { + adapter: inspectorAdapters.requests, + title: i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { + defaultMessage: 'Data', + }), + description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the visualization.', + }), + }, + }) + .toPromise(); const parsedTimeRange = timeRange ? calculateBounds(timeRange, { forceNow }) : null; const tabifyParams = { - metricsAtAllLevels, + metricsAtAllLevels: aggs.hierarchical, partialRows, timeRange: parsedTimeRange ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } diff --git a/src/plugins/data/common/search/search_source/inspect/inspector_stats.ts b/src/plugins/data/common/search/search_source/inspect/inspector_stats.ts index 24507a7e13058..e5a3acc23eee8 100644 --- a/src/plugins/data/common/search/search_source/inspect/inspector_stats.ts +++ b/src/plugins/data/common/search/search_source/inspect/inspector_stats.ts @@ -50,7 +50,7 @@ export function getRequestInspectorStats(searchSource: ISearchSource) { /** @public */ export function getResponseInspectorStats( - resp: estypes.SearchResponse, + resp?: estypes.SearchResponse, searchSource?: ISearchSource ) { const lastRequest = diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 3726e5d0c33e8..7f8a4fceff05d 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -11,6 +11,10 @@ import { IndexPattern } from '../../index_patterns'; import { GetConfigFn } from '../../types'; import { fetchSoon } from './legacy'; import { SearchSource, SearchSourceDependencies, SortDirection } from './'; +import { AggConfigs, AggTypesRegistryStart } from '../../'; +import { mockAggTypesRegistry } from '../aggs/test_helpers'; +import { RequestResponder } from 'src/plugins/inspector/common'; +import { switchMap } from 'rxjs/operators'; jest.mock('./legacy', () => ({ fetchSoon: jest.fn().mockResolvedValue({}), @@ -39,6 +43,21 @@ const indexPattern2 = ({ getSourceFiltering: () => mockSource2, } as unknown) as IndexPattern; +const fields3 = [{ name: 'foo-bar' }, { name: 'field1' }, { name: 'field2' }]; +const indexPattern3 = ({ + title: 'foo', + fields: { + getByName: (name: string) => { + return fields3.find((field) => field.name === name); + }, + filter: () => { + return fields3; + }, + }, + getComputedFields, + getSourceFiltering: () => mockSource, +} as unknown) as IndexPattern; + const runtimeFieldDef = { type: 'keyword', script: { @@ -61,8 +80,8 @@ describe('SearchSource', () => { .fn() .mockReturnValue( of( - { rawResponse: { isPartial: true, isRunning: true } }, - { rawResponse: { isPartial: false, isRunning: false } } + { rawResponse: { test: 1 }, isPartial: true, isRunning: true }, + { rawResponse: { test: 2 }, isPartial: false, isRunning: false } ) ); @@ -81,17 +100,19 @@ describe('SearchSource', () => { describe('#getField()', () => { test('gets the value for the property', () => { - searchSource.setField('aggs', 5); - expect(searchSource.getField('aggs')).toBe(5); + searchSource.setField('aggs', { i: 5 }); + expect(searchSource.getField('aggs')).toStrictEqual({ i: 5 }); }); }); describe('#getFields()', () => { test('gets the value for the property', () => { - searchSource.setField('aggs', 5); + searchSource.setField('aggs', { i: 5 }); expect(searchSource.getFields()).toMatchInlineSnapshot(` Object { - "aggs": 5, + "aggs": Object { + "i": 5, + }, } `); }); @@ -100,7 +121,7 @@ describe('SearchSource', () => { describe('#removeField()', () => { test('remove property', () => { searchSource = new SearchSource({}, searchSourceDependencies); - searchSource.setField('aggs', 5); + searchSource.setField('aggs', { i: 5 }); searchSource.removeField('aggs'); expect(searchSource.getField('aggs')).toBeFalsy(); }); @@ -108,8 +129,20 @@ describe('SearchSource', () => { describe('#setField() / #flatten', () => { test('sets the value for the property', () => { - searchSource.setField('aggs', 5); - expect(searchSource.getField('aggs')).toBe(5); + searchSource.setField('aggs', { i: 5 }); + expect(searchSource.getField('aggs')).toStrictEqual({ i: 5 }); + }); + + test('sets the value for the property with AggConfigs', () => { + const typesRegistry = mockAggTypesRegistry(); + + const ac = new AggConfigs(indexPattern3, [{ type: 'avg', params: { field: 'field1' } }], { + typesRegistry, + }); + + searchSource.setField('aggs', ac); + const request = searchSource.getSearchRequestBody(); + expect(request.aggs).toStrictEqual({ '1': { avg: { field: 'field1' } } }); }); describe('computed fields handling', () => { @@ -631,7 +664,7 @@ describe('SearchSource', () => { const fn = jest.fn(); searchSource.onRequestStart(fn); const options = {}; - await searchSource.fetch(options); + await searchSource.fetch$(options).toPromise(); expect(fn).toBeCalledWith(searchSource, options); }); @@ -644,7 +677,7 @@ describe('SearchSource', () => { const parentFn = jest.fn(); parent.onRequestStart(parentFn); const options = {}; - await searchSource.fetch(options); + await searchSource.fetch$(options).toPromise(); expect(fn).toBeCalledWith(searchSource, options); expect(parentFn).not.toBeCalled(); @@ -664,69 +697,13 @@ describe('SearchSource', () => { const parentFn = jest.fn(); parent.onRequestStart(parentFn); const options = {}; - await searchSource.fetch(options); + await searchSource.fetch$(options).toPromise(); expect(fn).toBeCalledWith(searchSource, options); expect(parentFn).toBeCalledWith(searchSource, options); }); }); - describe('#legacy fetch()', () => { - beforeEach(() => { - searchSourceDependencies = { - ...searchSourceDependencies, - getConfig: jest.fn(() => { - return true; // batchSearches = true - }) as GetConfigFn, - }; - }); - - test('should call msearch', async () => { - searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); - const options = {}; - await searchSource.fetch(options); - expect(fetchSoon).toBeCalledTimes(1); - }); - }); - - describe('#search service fetch()', () => { - test('should call msearch', async () => { - searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); - const options = {}; - - await searchSource.fetch(options); - expect(mockSearchMethod).toBeCalledTimes(1); - }); - - test('should return partial results', (done) => { - searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); - const options = {}; - - const next = jest.fn(); - const complete = () => { - expect(next).toBeCalledTimes(2); - expect(next.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "isPartial": true, - "isRunning": true, - }, - ] - `); - expect(next.mock.calls[1]).toMatchInlineSnapshot(` - Array [ - Object { - "isPartial": false, - "isRunning": false, - }, - ] - `); - done(); - }; - searchSource.fetch$(options).subscribe({ next, complete }); - }); - }); - describe('#serialize', () => { test('should reference index patterns', () => { const indexPattern123 = { id: '123' } as IndexPattern; @@ -884,4 +861,373 @@ describe('SearchSource', () => { ); }); }); + + describe('fetch$', () => { + describe('#legacy fetch()', () => { + beforeEach(() => { + searchSourceDependencies = { + ...searchSourceDependencies, + getConfig: jest.fn(() => { + return true; // batchSearches = true + }) as GetConfigFn, + }; + }); + + test('should call msearch', async () => { + searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); + const options = {}; + await searchSource.fetch$(options).toPromise(); + expect(fetchSoon).toBeCalledTimes(1); + }); + }); + + describe('responses', () => { + test('should return partial results', async () => { + searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); + const options = {}; + + const next = jest.fn(); + const complete = jest.fn(); + const res$ = searchSource.fetch$(options); + res$.subscribe({ next, complete }); + await res$.toPromise(); + + expect(next).toBeCalledTimes(2); + expect(complete).toBeCalledTimes(1); + expect(next.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "test": 1, + }, + ] + `); + expect(next.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "test": 2, + }, + ] + `); + }); + + test('shareReplays result', async () => { + searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); + const options = {}; + + const next = jest.fn(); + const complete = jest.fn(); + const next2 = jest.fn(); + const complete2 = jest.fn(); + const res$ = searchSource.fetch$(options); + res$.subscribe({ next, complete }); + res$.subscribe({ next: next2, complete: complete2 }); + await res$.toPromise(); + + expect(next).toBeCalledTimes(2); + expect(next2).toBeCalledTimes(2); + expect(complete).toBeCalledTimes(1); + expect(complete2).toBeCalledTimes(1); + expect(searchSourceDependencies.search).toHaveBeenCalledTimes(1); + }); + + test('should emit error on empty response', async () => { + searchSourceDependencies.search = mockSearchMethod = jest + .fn() + .mockReturnValue( + of({ rawResponse: { test: 1 }, isPartial: true, isRunning: true }, undefined) + ); + + searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); + const options = {}; + + const next = jest.fn(); + const error = jest.fn(); + const complete = jest.fn(); + const res$ = searchSource.fetch$(options); + res$.subscribe({ next, error, complete }); + await res$.toPromise().catch((e) => {}); + + expect(next).toBeCalledTimes(1); + expect(error).toBeCalledTimes(1); + expect(complete).toBeCalledTimes(0); + expect(next.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "test": 1, + }, + ] + `); + expect(error.mock.calls[0][0]).toBe(undefined); + }); + }); + + describe('inspector', () => { + let requestResponder: RequestResponder; + beforeEach(() => { + requestResponder = ({ + stats: jest.fn(), + ok: jest.fn(), + error: jest.fn(), + json: jest.fn(), + } as unknown) as RequestResponder; + }); + + test('calls inspector if provided', async () => { + const options = { + inspector: { + title: 'a', + adapter: { + start: jest.fn().mockReturnValue(requestResponder), + } as any, + }, + }; + + searchSource = new SearchSource({}, searchSourceDependencies); + searchSource.setField('index', indexPattern); + await searchSource.fetch$(options).toPromise(); + + expect(options.inspector.adapter.start).toBeCalledTimes(1); + expect(requestResponder.error).not.toBeCalled(); + expect(requestResponder.json).toBeCalledTimes(1); + expect(requestResponder.ok).toBeCalledTimes(1); + // First and last + expect(requestResponder.stats).toBeCalledTimes(2); + }); + + test('calls inspector only once, with multiple subs (shareReplay)', async () => { + const options = { + inspector: { + title: 'a', + adapter: { + start: jest.fn().mockReturnValue(requestResponder), + } as any, + }, + }; + + searchSource = new SearchSource({}, searchSourceDependencies); + searchSource.setField('index', indexPattern); + const res$ = searchSource.fetch$(options); + + const complete1 = jest.fn(); + const complete2 = jest.fn(); + + res$.subscribe({ + complete: complete1, + }); + res$.subscribe({ + complete: complete2, + }); + + await res$.toPromise(); + + expect(complete1).toBeCalledTimes(1); + expect(complete2).toBeCalledTimes(1); + expect(options.inspector.adapter.start).toBeCalledTimes(1); + }); + + test('calls error on inspector', async () => { + const options = { + inspector: { + title: 'a', + adapter: { + start: jest.fn().mockReturnValue(requestResponder), + } as any, + }, + }; + + searchSourceDependencies.search = jest.fn().mockReturnValue(of(Promise.reject('aaaaa'))); + + searchSource = new SearchSource({}, searchSourceDependencies); + searchSource.setField('index', indexPattern); + await searchSource + .fetch$(options) + .toPromise() + .catch(() => {}); + + expect(options.inspector.adapter.start).toBeCalledTimes(1); + expect(requestResponder.json).toBeCalledTimes(1); + expect(requestResponder.error).toBeCalledTimes(1); + expect(requestResponder.ok).toBeCalledTimes(0); + expect(requestResponder.stats).toBeCalledTimes(0); + }); + }); + + describe('postFlightRequest', () => { + let fetchSub: any; + + function getAggConfigs(typesRegistry: AggTypesRegistryStart, enabled: boolean) { + return new AggConfigs( + indexPattern3, + [ + { + type: 'avg', + enabled, + params: { field: 'field1' }, + }, + ], + { + typesRegistry, + } + ); + } + + beforeEach(() => { + fetchSub = { + next: jest.fn(), + complete: jest.fn(), + error: jest.fn(), + }; + }); + + test('doesnt call any post flight requests if disabled', async () => { + const typesRegistry = mockAggTypesRegistry(); + typesRegistry.get('avg').postFlightRequest = jest.fn(); + const ac = getAggConfigs(typesRegistry, false); + + searchSource = new SearchSource({}, searchSourceDependencies); + searchSource.setField('index', indexPattern); + searchSource.setField('aggs', ac); + const fetch$ = searchSource.fetch$({}); + fetch$.subscribe(fetchSub); + await fetch$.toPromise(); + + expect(fetchSub.next).toHaveBeenCalledTimes(2); + expect(fetchSub.complete).toHaveBeenCalledTimes(1); + expect(fetchSub.error).toHaveBeenCalledTimes(0); + + expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(0); + }); + + test('doesnt call any post flight if searchsource has error', async () => { + const typesRegistry = mockAggTypesRegistry(); + typesRegistry.get('avg').postFlightRequest = jest.fn(); + const ac = getAggConfigs(typesRegistry, true); + + searchSourceDependencies.search = jest.fn().mockImplementation(() => + of(1).pipe( + switchMap((r) => { + throw r; + }) + ) + ); + + searchSource = new SearchSource({}, searchSourceDependencies); + searchSource.setField('index', indexPattern); + searchSource.setField('aggs', ac); + const fetch$ = searchSource.fetch$({}); + fetch$.subscribe(fetchSub); + await fetch$.toPromise().catch((e) => {}); + + expect(fetchSub.next).toHaveBeenCalledTimes(0); + expect(fetchSub.complete).toHaveBeenCalledTimes(0); + expect(fetchSub.error).toHaveBeenNthCalledWith(1, 1); + + expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(0); + }); + + test('calls post flight requests, fires 1 extra response, returns last response', async () => { + const typesRegistry = mockAggTypesRegistry(); + typesRegistry.get('avg').postFlightRequest = jest.fn().mockResolvedValue({ + other: 5, + }); + + const allac = new AggConfigs( + indexPattern3, + [ + { + type: 'avg', + enabled: true, + params: { field: 'field1' }, + }, + { + type: 'avg', + enabled: true, + params: { field: 'field2' }, + }, + { + type: 'avg', + enabled: true, + params: { field: 'foo-bar' }, + }, + ], + { + typesRegistry, + } + ); + + searchSource = new SearchSource({}, searchSourceDependencies); + searchSource.setField('index', indexPattern); + searchSource.setField('aggs', allac); + const fetch$ = searchSource.fetch$({}); + fetch$.subscribe(fetchSub); + + const resp = await fetch$.toPromise(); + + expect(fetchSub.next).toHaveBeenCalledTimes(3); + expect(fetchSub.complete).toHaveBeenCalledTimes(1); + expect(fetchSub.error).toHaveBeenCalledTimes(0); + expect(resp).toStrictEqual({ other: 5 }); + expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(3); + }); + + test('calls post flight requests only once, with multiple subs (shareReplay)', async () => { + const typesRegistry = mockAggTypesRegistry(); + typesRegistry.get('avg').postFlightRequest = jest.fn().mockResolvedValue({ + other: 5, + }); + + const allac = new AggConfigs( + indexPattern3, + [ + { + type: 'avg', + enabled: true, + params: { field: 'field1' }, + }, + ], + { + typesRegistry, + } + ); + + searchSource = new SearchSource({}, searchSourceDependencies); + searchSource.setField('index', indexPattern); + searchSource.setField('aggs', allac); + const fetch$ = searchSource.fetch$({}); + fetch$.subscribe(fetchSub); + + const fetchSub2 = { + next: jest.fn(), + complete: jest.fn(), + error: jest.fn(), + }; + fetch$.subscribe(fetchSub2); + + await fetch$.toPromise(); + + expect(fetchSub.next).toHaveBeenCalledTimes(3); + expect(fetchSub.complete).toHaveBeenCalledTimes(1); + expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(1); + }); + + test('calls post flight requests, handles error', async () => { + const typesRegistry = mockAggTypesRegistry(); + typesRegistry.get('avg').postFlightRequest = jest.fn().mockRejectedValue(undefined); + const ac = getAggConfigs(typesRegistry, true); + + searchSource = new SearchSource({}, searchSourceDependencies); + searchSource.setField('index', indexPattern); + searchSource.setField('aggs', ac); + const fetch$ = searchSource.fetch$({}); + fetch$.subscribe(fetchSub); + + await fetch$.toPromise().catch(() => {}); + + expect(fetchSub.next).toHaveBeenCalledTimes(2); + expect(fetchSub.complete).toHaveBeenCalledTimes(0); + expect(fetchSub.error).toHaveBeenCalledTimes(1); + expect(typesRegistry.get('avg').postFlightRequest).toHaveBeenCalledTimes(1); + }); + }); + }); }); diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index e1e7a8292d677..1c1c32228703f 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -60,12 +60,22 @@ import { setWith } from '@elastic/safer-lodash-set'; import { uniqueId, keyBy, pick, difference, isFunction, isEqual, uniqWith, isObject } from 'lodash'; -import { catchError, finalize, map, switchMap, tap } from 'rxjs/operators'; -import { defer, from } from 'rxjs'; +import { + catchError, + finalize, + first, + last, + map, + shareReplay, + switchMap, + tap, +} from 'rxjs/operators'; +import { defer, EMPTY, from, Observable } from 'rxjs'; +import { estypes } from '@elastic/elasticsearch'; import { normalizeSortRequest } from './normalize_sort_request'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; import { IIndexPattern, IndexPattern, IndexPatternField } from '../../index_patterns'; -import { ISearchGeneric, ISearchOptions } from '../..'; +import { AggConfigs, ISearchGeneric, ISearchOptions } from '../..'; import type { ISearchSource, SearchFieldValue, @@ -75,7 +85,15 @@ import type { import { FetchHandlers, RequestFailure, getSearchParamsFromRequest, SearchRequest } from './fetch'; import { getRequestInspectorStats, getResponseInspectorStats } from './inspect'; -import { getEsQueryConfig, buildEsQuery, Filter, UI_SETTINGS } from '../../../common'; +import { + getEsQueryConfig, + buildEsQuery, + Filter, + UI_SETTINGS, + isErrorResponse, + isPartialResponse, + IKibanaSearchResponse, +} from '../../../common'; import { getHighlightRequest } from '../../../common/field_formats'; import { fetchSoon } from './legacy'; import { extractReferences } from './extract_references'; @@ -256,10 +274,8 @@ export class SearchSource { */ fetch$(options: ISearchOptions = {}) { const { getConfig } = this.dependencies; - return defer(() => this.requestIsStarting(options)).pipe( - tap(() => { - options.requestResponder?.stats(getRequestInspectorStats(this)); - }), + + const s$ = defer(() => this.requestIsStarting(options)).pipe( switchMap(() => { const searchRequest = this.flatten(); this.history = [searchRequest]; @@ -273,21 +289,14 @@ export class SearchSource { }), tap((response) => { // TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved - if ((response as any).error) { + if (!response || (response as any).error) { throw new RequestFailure(null, response); - } else { - options.requestResponder?.stats(getResponseInspectorStats(response, this)); - options.requestResponder?.ok({ json: response }); } }), - catchError((e) => { - options.requestResponder?.error({ json: e }); - throw e; - }), - finalize(() => { - options.requestResponder?.json(this.getSearchRequestBody()); - }) + shareReplay() ); + + return this.inspectSearch(s$, options); } /** @@ -328,9 +337,96 @@ export class SearchSource { * PRIVATE APIS ******/ + private inspectSearch(s$: Observable>, options: ISearchOptions) { + const { id, title, description, adapter } = options.inspector || { title: '' }; + + const requestResponder = adapter?.start(title, { + id, + description, + searchSessionId: options.sessionId, + }); + + const trackRequestBody = () => { + try { + requestResponder?.json(this.getSearchRequestBody()); + } catch (e) {} // eslint-disable-line no-empty + }; + + // Track request stats on first emit, swallow errors + const first$ = s$ + .pipe( + first(undefined, null), + tap(() => { + requestResponder?.stats(getRequestInspectorStats(this)); + trackRequestBody(); + }), + catchError(() => { + trackRequestBody(); + return EMPTY; + }), + finalize(() => { + first$.unsubscribe(); + }) + ) + .subscribe(); + + // Track response stats on last emit, as well as errors + const last$ = s$ + .pipe( + catchError((e) => { + requestResponder?.error({ json: e }); + return EMPTY; + }), + last(undefined, null), + tap((finalResponse) => { + if (finalResponse) { + requestResponder?.stats(getResponseInspectorStats(finalResponse, this)); + requestResponder?.ok({ json: finalResponse }); + } + }), + finalize(() => { + last$.unsubscribe(); + }) + ) + .subscribe(); + + return s$; + } + + private hasPostFlightRequests() { + const aggs = this.getField('aggs'); + if (aggs instanceof AggConfigs) { + return aggs.aggs.some( + (agg) => agg.enabled && typeof agg.type.postFlightRequest === 'function' + ); + } else { + return false; + } + } + + private async fetchOthers(response: estypes.SearchResponse, options: ISearchOptions) { + const aggs = this.getField('aggs'); + if (aggs instanceof AggConfigs) { + for (const agg of aggs.aggs) { + if (agg.enabled && typeof agg.type.postFlightRequest === 'function') { + response = await agg.type.postFlightRequest( + response, + aggs, + agg, + this, + options.inspector?.adapter, + options.abortSignal, + options.sessionId + ); + } + } + return response; + } + } + /** * Run a search using the search service - * @return {Promise>} + * @return {Observable>} */ private fetchSearch$(searchRequest: SearchRequest, options: ISearchOptions) { const { search, getConfig, onResponse } = this.dependencies; @@ -340,6 +436,43 @@ export class SearchSource { }); return search({ params, indexType: searchRequest.indexType }, options).pipe( + switchMap((response) => { + return new Observable>((obs) => { + if (isErrorResponse(response)) { + obs.error(response); + } else if (isPartialResponse(response)) { + obs.next(response); + } else { + if (!this.hasPostFlightRequests()) { + obs.next(response); + obs.complete(); + } else { + // Treat the complete response as partial, then run the postFlightRequests. + obs.next({ + ...response, + isPartial: true, + isRunning: true, + }); + const sub = from(this.fetchOthers(response.rawResponse, options)).subscribe({ + next: (responseWithOther) => { + obs.next({ + ...response, + rawResponse: responseWithOther, + }); + }, + error: (e) => { + obs.error(e); + sub.unsubscribe(); + }, + complete: () => { + obs.complete(); + sub.unsubscribe(); + }, + }); + } + } + }); + }), map(({ rawResponse }) => onResponse(searchRequest, rawResponse)) ); } @@ -452,6 +585,12 @@ export class SearchSource { getConfig(UI_SETTINGS.SORT_OPTIONS) ); return addToBody(key, sort); + case 'aggs': + if ((val as any) instanceof AggConfigs) { + return addToBody('aggs', val.toDsl()); + } else { + return addToBody('aggs', val); + } default: return addToBody(key, val); } diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts index a178b38693d92..99f3f67a5e257 100644 --- a/src/plugins/data/common/search/search_source/types.ts +++ b/src/plugins/data/common/search/search_source/types.ts @@ -7,6 +7,7 @@ */ import { NameList } from 'elasticsearch'; +import { IAggConfigs } from 'src/plugins/data/public'; import { Query } from '../..'; import { Filter } from '../../es_query'; import { IndexPattern } from '../../index_patterns'; @@ -78,7 +79,7 @@ export interface SearchSourceFields { /** * {@link AggConfigs} */ - aggs?: any; + aggs?: object | IAggConfigs | (() => object); from?: number; size?: number; source?: NameList; diff --git a/src/plugins/data/common/search/tabify/index.ts b/src/plugins/data/common/search/tabify/index.ts index 168d4cf9d4c37..74fbc7ba4cfa4 100644 --- a/src/plugins/data/common/search/tabify/index.ts +++ b/src/plugins/data/common/search/tabify/index.ts @@ -6,27 +6,6 @@ * Side Public License, v 1. */ -import { SearchResponse } from 'elasticsearch'; -import { SearchSource } from '../search_source'; -import { tabifyAggResponse } from './tabify'; -import { tabifyDocs, TabifyDocsOptions } from './tabify_docs'; -import { TabbedResponseWriterOptions } from './types'; - -export const tabify = ( - searchSource: SearchSource, - esResponse: SearchResponse, - opts: Partial | TabifyDocsOptions -) => { - return !esResponse.aggregations - ? tabifyDocs(esResponse, searchSource.getField('index'), opts as TabifyDocsOptions) - : tabifyAggResponse( - searchSource.getField('aggs'), - esResponse, - opts as Partial - ); -}; - -export { tabifyDocs }; - +export { tabifyDocs } from './tabify_docs'; export { tabifyAggResponse } from './tabify'; export { tabifyGetColumns } from './get_columns'; diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index 37de8dc49d3c6..e3ec499a0020d 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -9,7 +9,7 @@ import { Observable } from 'rxjs'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; import { IndexPattern } from '..'; -import type { RequestResponder } from '../../../inspector/common'; +import type { RequestAdapter } from '../../../inspector/common'; export type ISearchGeneric = < SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, @@ -81,6 +81,13 @@ export interface IKibanaSearchRequest { params?: Params; } +export interface IInspectorInfo { + adapter?: RequestAdapter; + title: string; + id?: string; + description?: string; +} + export interface ISearchOptions { /** * An `AbortSignal` that allows the caller of `search` to abort a search request. @@ -117,10 +124,12 @@ export interface ISearchOptions { /** * Index pattern reference is used for better error messages */ - indexPattern?: IndexPattern; - requestResponder?: RequestResponder; + /** + * Inspector integration options + */ + inspector?: IInspectorInfo; } /** diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 35f13fc855e99..0dd06691d68bb 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -46,6 +46,7 @@ import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_ import { History } from 'history'; import { Href } from 'history'; import { HttpSetup } from 'kibana/public'; +import { IAggConfigs as IAggConfigs_2 } from 'src/plugins/data/public'; import { IconType } from '@elastic/eui'; import { IncomingHttpHeaders } from 'http'; import { InjectedIntl } from '@kbn/i18n/react'; @@ -254,6 +255,8 @@ export class AggConfigs { getResponseAggById(id: string): AggConfig | undefined; getResponseAggs(): AggConfig[]; // (undocumented) + hierarchical?: boolean; + // (undocumented) indexPattern: IndexPattern; jsonDataEquals(aggConfigs: AggConfig[]): boolean; // (undocumented) @@ -267,7 +270,7 @@ export class AggConfigs { // (undocumented) timeRange?: TimeRange; // (undocumented) - toDsl(hierarchical?: boolean): Record; + toDsl(): Record; } // @internal (undocumented) @@ -1672,13 +1675,11 @@ export type ISearchGeneric = >; + fetch$(options?: ISearchOptions): Observable>; // @deprecated - fetch(options?: ISearchOptions): Promise>; + fetch(options?: ISearchOptions): Promise>; getField(field: K, recurse?: boolean): SearchSourceFields[K]; getFields(): SearchSourceFields; getId(): string; @@ -2462,7 +2463,7 @@ export class SearchSource { // @public export interface SearchSourceFields { // (undocumented) - aggs?: any; + aggs?: object | IAggConfigs_2 | (() => object); // Warning: (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts fields?: SearchFieldValue[]; // @deprecated diff --git a/src/plugins/data/public/search/expressions/esaggs.test.ts b/src/plugins/data/public/search/expressions/esaggs.test.ts index d7a6446781c43..e75bd7be219de 100644 --- a/src/plugins/data/public/search/expressions/esaggs.test.ts +++ b/src/plugins/data/public/search/expressions/esaggs.test.ts @@ -100,17 +100,20 @@ describe('esaggs expression function - public', () => { expect(handleEsaggsRequest).toHaveBeenCalledWith({ abortSignal: mockHandlers.abortSignal, - aggs: { foo: 'bar' }, + aggs: { + foo: 'bar', + hierarchical: true, + }, filters: undefined, indexPattern: {}, inspectorAdapters: mockHandlers.inspectorAdapters, - metricsAtAllLevels: args.metricsAtAllLevels, partialRows: args.partialRows, query: undefined, searchSessionId: 'abc123', searchSourceService: startDependencies.searchSource, timeFields: args.timeFields, timeRange: undefined, + getNow: undefined, }); }); diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 45d24af3a6ebb..1e3d56c71e423 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -8,7 +8,6 @@ import { get } from 'lodash'; import { StartServicesAccessor } from 'src/core/public'; -import { Adapters } from 'src/plugins/inspector/common'; import { EsaggsExpressionFunctionDefinition, EsaggsStartDependencies, @@ -44,14 +43,14 @@ export function getFunctionDefinition({ indexPattern, args.aggs!.map((agg) => agg.value) ); + aggConfigs.hierarchical = args.metricsAtAllLevels; return await handleEsaggsRequest({ - abortSignal: (abortSignal as unknown) as AbortSignal, + abortSignal, aggs: aggConfigs, filters: get(input, 'filters', undefined), indexPattern, - inspectorAdapters: inspectorAdapters as Adapters, - metricsAtAllLevels: args.metricsAtAllLevels, + inspectorAdapters, partialRows: args.partialRows, query: get(input, 'query', undefined) as any, searchSessionId: getSearchSessionId(), diff --git a/src/plugins/data/server/search/expressions/esaggs.test.ts b/src/plugins/data/server/search/expressions/esaggs.test.ts index 124a171de6378..15287e9d8cf5b 100644 --- a/src/plugins/data/server/search/expressions/esaggs.test.ts +++ b/src/plugins/data/server/search/expressions/esaggs.test.ts @@ -108,11 +108,13 @@ describe('esaggs expression function - server', () => { expect(handleEsaggsRequest).toHaveBeenCalledWith({ abortSignal: mockHandlers.abortSignal, - aggs: { foo: 'bar' }, + aggs: { + foo: 'bar', + hierarchical: args.metricsAtAllLevels, + }, filters: undefined, indexPattern: {}, inspectorAdapters: mockHandlers.inspectorAdapters, - metricsAtAllLevels: args.metricsAtAllLevels, partialRows: args.partialRows, query: undefined, searchSessionId: 'abc123', diff --git a/src/plugins/data/server/search/expressions/esaggs.ts b/src/plugins/data/server/search/expressions/esaggs.ts index 61fd320d89b95..bb22a491b157e 100644 --- a/src/plugins/data/server/search/expressions/esaggs.ts +++ b/src/plugins/data/server/search/expressions/esaggs.ts @@ -9,7 +9,6 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { KibanaRequest, StartServicesAccessor } from 'src/core/server'; -import { Adapters } from 'src/plugins/inspector/common'; import { EsaggsExpressionFunctionDefinition, EsaggsStartDependencies, @@ -61,13 +60,14 @@ export function getFunctionDefinition({ args.aggs!.map((agg) => agg.value) ); + aggConfigs.hierarchical = args.metricsAtAllLevels; + return await handleEsaggsRequest({ - abortSignal: (abortSignal as unknown) as AbortSignal, + abortSignal, aggs: aggConfigs, filters: get(input, 'filters', undefined), indexPattern, - inspectorAdapters: inspectorAdapters as Adapters, - metricsAtAllLevels: args.metricsAtAllLevels, + inspectorAdapters, partialRows: args.partialRows, query: get(input, 'query', undefined) as any, searchSessionId: getSearchSessionId(), diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 622356c4441ac..3316e8102e50a 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -26,12 +26,14 @@ import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; import { estypes } from '@elastic/elasticsearch'; +import { EventEmitter } from 'events'; import { ExecutionContext } from 'src/plugins/expressions/common'; import { ExpressionAstExpression } from 'src/plugins/expressions/common'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils'; +import { IAggConfigs as IAggConfigs_2 } from 'src/plugins/data/public'; import { ISavedObjectsRepository } from 'src/core/server'; import { IScopedClusterClient } from 'src/core/server'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; @@ -999,13 +1001,11 @@ export interface IScopedSearchClient extends ISearchClient { export interface ISearchOptions { abortSignal?: AbortSignal; indexPattern?: IndexPattern; + // Warning: (ae-forgotten-export) The symbol "IInspectorInfo" needs to be exported by the entry point index.d.ts + inspector?: IInspectorInfo; isRestore?: boolean; isStored?: boolean; legacyHitsTotal?: boolean; - // Warning: (ae-forgotten-export) The symbol "RequestResponder" needs to be exported by the entry point index.d.ts - // - // (undocumented) - requestResponder?: RequestResponder; sessionId?: string; strategy?: string; } diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 35a89eb45f35e..4099d5e8ef7e2 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -415,11 +415,20 @@ function discoverController($route, $scope) { $scope.fetchStatus = fetchStatuses.LOADING; $scope.resultState = getResultState($scope.fetchStatus, $scope.rows); + inspectorAdapters.requests.reset(); return $scope.volatileSearchSource .fetch$({ abortSignal: abortController.signal, sessionId: searchSessionId, - requestResponder: getRequestResponder({ searchSessionId }), + inspector: { + adapter: inspectorAdapters.requests, + title: i18n.translate('discover.inspectorRequestDataTitle', { + defaultMessage: 'data', + }), + description: i18n.translate('discover.inspectorRequestDescription', { + defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', + }), + }, }) .toPromise() .then(onResults) @@ -465,17 +474,6 @@ function discoverController($route, $scope) { await refetch$.next(); }; - function getRequestResponder({ searchSessionId = null } = { searchSessionId: null }) { - inspectorAdapters.requests.reset(); - const title = i18n.translate('discover.inspectorRequestDataTitle', { - defaultMessage: 'data', - }); - const description = i18n.translate('discover.inspectorRequestDescription', { - defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', - }); - return inspectorAdapters.requests.start(title, { description, searchSessionId }); - } - $scope.resetQuery = function () { history.push( $route.current.params.id ? `/view/${encodeURIComponent($route.current.params.id)}` : '/' diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 237da72ae3a52..dbaf07fed18c2 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -317,17 +317,6 @@ export class SearchEmbeddable // Log request to inspector this.inspectorAdapters.requests!.reset(); - const title = i18n.translate('discover.embeddable.inspectorRequestDataTitle', { - defaultMessage: 'Data', - }); - const description = i18n.translate('discover.embeddable.inspectorRequestDescription', { - defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', - }); - - const requestResponder = this.inspectorAdapters.requests!.start(title, { - description, - searchSessionId, - }); this.searchScope.$apply(() => { this.searchScope!.isLoading = true; @@ -340,7 +329,16 @@ export class SearchEmbeddable .fetch$({ abortSignal: this.abortController.signal, sessionId: searchSessionId, - requestResponder, + inspector: { + adapter: this.inspectorAdapters.requests, + title: i18n.translate('discover.embeddable.inspectorRequestDataTitle', { + defaultMessage: 'Data', + }), + description: i18n.translate('discover.embeddable.inspectorRequestDescription', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the search.', + }), + }, }) .toPromise(); this.updateOutput({ loading: false, error: undefined }); diff --git a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts index 27164b3cddbc2..b260c594591fa 100644 --- a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts +++ b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts @@ -37,11 +37,14 @@ export const defaultEmbeddableFactoryProvider = < type: def.type, isEditable: def.isEditable.bind(def), getDisplayName: def.getDisplayName.bind(def), + getDescription: def.getDescription ? def.getDescription.bind(def) : () => '', + getIconType: def.getIconType ? def.getIconType.bind(def) : () => 'empty', savedObjectMetaData: def.savedObjectMetaData, telemetry: def.telemetry || (() => ({})), inject: def.inject || ((state: EmbeddableStateWithType) => state), extract: def.extract || ((state: EmbeddableStateWithType) => ({ state, references: [] })), migrations: def.migrations || {}, + grouping: def.grouping, }; return factory; }; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index 7f3277130f90f..6ec035f442dd2 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -14,6 +14,7 @@ import { IContainer } from '../containers/i_container'; import { PropertySpec } from '../types'; import { PersistableState } from '../../../../kibana_utils/common'; import { EmbeddableStateWithType } from '../../../common/types'; +import { UiActionsPresentableGrouping } from '../../../../ui_actions/public'; export interface EmbeddableInstanceConfiguration { id: string; @@ -48,6 +49,12 @@ export interface EmbeddableFactory< readonly savedObjectMetaData?: SavedObjectMetaData; + /** + * Indicates the grouping this factory should appear in a sub-menu. Example, this is used for grouping + * options in the editors menu in Dashboard for creating new embeddables + */ + readonly grouping?: UiActionsPresentableGrouping; + /** * True if is this factory create embeddables that are Containers. Used in the add panel to * conditionally show whether these can be added to another container. It's just not @@ -62,6 +69,16 @@ export interface EmbeddableFactory< */ getDisplayName(): string; + /** + * Returns an EUI Icon type to be displayed in a menu. + */ + getIconType(): string; + + /** + * Returns a description about the embeddable. + */ + getDescription(): string; + /** * If false, this type of embeddable can't be created with the "createNew" functionality. Instead, * use createFromSavedObject, where an existing saved object must first exist. diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts index a64aa32c6e7c4..f2819f2a2e664 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts @@ -33,5 +33,8 @@ export type EmbeddableFactoryDefinition< | 'extract' | 'inject' | 'migrations' + | 'grouping' + | 'getIconType' + | 'getDescription' > >; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx index 432897763aa04..1c96945f014c8 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -61,6 +61,7 @@ test('createNewEmbeddable() add embeddable to container', async () => { getAllFactories={start.getEmbeddableFactories} notifications={core.notifications} SavedObjectFinder={() => null} + showCreateNewMenu /> ) as ReactWrapper; @@ -112,6 +113,7 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()' getAllFactories={start.getEmbeddableFactories} notifications={core.notifications} SavedObjectFinder={(props) => } + showCreateNewMenu /> ) as ReactWrapper; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 8caec4a4428c3..6d6a68d7e5e2a 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -26,6 +26,7 @@ interface Props { getAllFactories: EmbeddableStart['getEmbeddableFactories']; notifications: CoreSetup['notifications']; SavedObjectFinder: React.ComponentType; + showCreateNewMenu?: boolean; } interface State { @@ -134,7 +135,9 @@ export class AddPanelFlyout extends React.Component { defaultMessage: 'No matching objects found.', })} > - + {this.props.showCreateNewMenu ? ( + + ) : null} ); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx index bed97c82095c7..f0c6e81644b3d 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -20,6 +20,7 @@ export function openAddPanelFlyout(options: { overlays: OverlayStart; notifications: NotificationsStart; SavedObjectFinder: React.ComponentType; + showCreateNewMenu?: boolean; }): OverlayRef { const { embeddable, @@ -28,6 +29,7 @@ export function openAddPanelFlyout(options: { overlays, notifications, SavedObjectFinder, + showCreateNewMenu, } = options; const flyoutSession = overlays.openFlyout( toMountPoint( @@ -42,6 +44,7 @@ export function openAddPanelFlyout(options: { getAllFactories={getAllFactories} notifications={notifications} SavedObjectFinder={SavedObjectFinder} + showCreateNewMenu={showCreateNewMenu} /> ), { diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 220039de2f34e..d522a4e5fa8e8 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -378,8 +378,12 @@ export interface EmbeddableFactory; createFromSavedObject(savedObjectId: string, input: Partial, parent?: IContainer): Promise; getDefaultInput(partial: Partial): Partial; + getDescription(): string; getDisplayName(): string; getExplicitInput(): Promise>; + getIconType(): string; + // Warning: (ae-forgotten-export) The symbol "PresentableGrouping" needs to be exported by the entry point index.d.ts + readonly grouping?: PresentableGrouping; readonly isContainerType: boolean; readonly isEditable: () => Promise; // Warning: (ae-forgotten-export) The symbol "SavedObjectMetaData" needs to be exported by the entry point index.d.ts @@ -393,7 +397,7 @@ export interface EmbeddableFactory = IEmbeddable, T extends SavedObjectAttributes = SavedObjectAttributes> = Pick, 'create' | 'type' | 'isEditable' | 'getDisplayName'> & Partial, 'createFromSavedObject' | 'isContainerType' | 'getExplicitInput' | 'savedObjectMetaData' | 'canCreateNew' | 'getDefaultInput' | 'telemetry' | 'extract' | 'inject' | 'migrations'>>; +export type EmbeddableFactoryDefinition = IEmbeddable, T extends SavedObjectAttributes = SavedObjectAttributes> = Pick, 'create' | 'type' | 'isEditable' | 'getDisplayName'> & Partial, 'createFromSavedObject' | 'isContainerType' | 'getExplicitInput' | 'savedObjectMetaData' | 'canCreateNew' | 'getDefaultInput' | 'telemetry' | 'extract' | 'inject' | 'migrations' | 'grouping' | 'getIconType' | 'getDescription'>>; // Warning: (ae-missing-release-tag) "EmbeddableFactoryNotFoundError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -724,6 +728,7 @@ export function openAddPanelFlyout(options: { overlays: OverlayStart_2; notifications: NotificationsStart_2; SavedObjectFinder: React.ComponentType; + showCreateNewMenu?: boolean; }): OverlayRef_2; // Warning: (ae-missing-release-tag) "OutputSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/home/server/services/sample_data/routes/install.ts b/src/plugins/home/server/services/sample_data/routes/install.ts index a20c3e350222f..e5ff33d5c199d 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -7,7 +7,7 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter, Logger, RequestHandlerContext } from 'src/core/server'; +import { IRouter, Logger, IScopedClusterClient } from 'src/core/server'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; import { createIndexName } from '../lib/create_index_name'; import { @@ -22,7 +22,7 @@ const insertDataIntoIndex = ( dataIndexConfig: any, index: string, nowReference: string, - context: RequestHandlerContext, + esClient: IScopedClusterClient, logger: Logger ) => { function updateTimestamps(doc: any) { @@ -51,9 +51,11 @@ const insertDataIntoIndex = ( bulk.push(insertCmd); bulk.push(updateTimestamps(doc)); }); - const resp = await context.core.elasticsearch.legacy.client.callAsCurrentUser('bulk', { + + const { body: resp } = await esClient.asCurrentUser.bulk({ body: bulk, }); + if (resp.errors) { const errMsg = `sample_data install errors while bulk inserting. Elasticsearch response: ${JSON.stringify( resp, @@ -100,7 +102,7 @@ export function createInstallRoute( // clean up any old installation of dataset try { - await context.core.elasticsearch.legacy.client.callAsCurrentUser('indices.delete', { + await context.core.elasticsearch.client.asCurrentUser.indices.delete({ index, }); } catch (err) { @@ -108,17 +110,13 @@ export function createInstallRoute( } try { - const createIndexParams = { + await context.core.elasticsearch.client.asCurrentUser.indices.create({ index, body: { settings: { index: { number_of_shards: 1, auto_expand_replicas: '0-1' } }, mappings: { properties: dataIndexConfig.fields }, }, - }; - await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'indices.create', - createIndexParams - ); + }); } catch (err) { const errMsg = `Unable to create sample data index "${index}", error: ${err.message}`; logger.warn(errMsg); @@ -130,7 +128,7 @@ export function createInstallRoute( dataIndexConfig, index, nowReference, - context, + context.core.elasticsearch.client, logger ); (counts as any)[index] = count; diff --git a/src/plugins/home/server/services/sample_data/routes/list.ts b/src/plugins/home/server/services/sample_data/routes/list.ts index 86e286644f936..72d8c31cbafd7 100644 --- a/src/plugins/home/server/services/sample_data/routes/list.ts +++ b/src/plugins/home/server/services/sample_data/routes/list.ts @@ -36,22 +36,20 @@ export const createListRoute = (router: IRouter, sampleDatasets: SampleDatasetSc const dataIndexConfig = sampleDataset.dataIndices[i]; const index = createIndexName(sampleDataset.id, dataIndexConfig.id); try { - const indexExists = await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'indices.exists', - { index } - ); + const { + body: indexExists, + } = await context.core.elasticsearch.client.asCurrentUser.indices.exists({ + index, + }); if (!indexExists) { sampleDataset.status = NOT_INSTALLED; return; } - const { count } = await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'count', - { - index, - } - ); - if (count === 0) { + const { body: count } = await context.core.elasticsearch.client.asCurrentUser.count({ + index, + }); + if (count.count === 0) { sampleDataset.status = NOT_INSTALLED; return; } diff --git a/src/plugins/home/server/services/sample_data/routes/uninstall.ts b/src/plugins/home/server/services/sample_data/routes/uninstall.ts index aa8ed67cf840a..3108c06492dd8 100644 --- a/src/plugins/home/server/services/sample_data/routes/uninstall.ts +++ b/src/plugins/home/server/services/sample_data/routes/uninstall.ts @@ -28,11 +28,7 @@ export function createUninstallRoute( async ( { core: { - elasticsearch: { - legacy: { - client: { callAsCurrentUser }, - }, - }, + elasticsearch: { client: esClient }, savedObjects: { getClient: getSavedObjectsClient, typeRegistry }, }, }, @@ -50,7 +46,9 @@ export function createUninstallRoute( const index = createIndexName(sampleDataset.id, dataIndexConfig.id); try { - await callAsCurrentUser('indices.delete', { index }); + await esClient.asCurrentUser.indices.delete({ + index, + }); } catch (err) { return response.customError({ statusCode: err.status, diff --git a/src/plugins/home/server/services/sample_data/usage/collector.ts b/src/plugins/home/server/services/sample_data/usage/collector.ts index 81958a2e3c878..df7d485c1f6fa 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector.ts @@ -6,22 +6,17 @@ * Side Public License, v 1. */ -import { PluginInitializerContext } from 'kibana/server'; -import { first } from 'rxjs/operators'; +import type { PluginInitializerContext } from 'kibana/server'; +import type { UsageCollectionSetup } from '../../../../../usage_collection/server'; import { fetchProvider, TelemetryResponse } from './collector_fetch'; -import { UsageCollectionSetup } from '../../../../../usage_collection/server'; -export async function makeSampleDataUsageCollector( +export function makeSampleDataUsageCollector( usageCollection: UsageCollectionSetup, context: PluginInitializerContext ) { - let index: string; - try { - const config = await context.config.legacy.globalConfig$.pipe(first()).toPromise(); - index = config.kibana.index; - } catch (err) { - return; // kibana plugin is not enabled (test environment) - } + const config = context.config.legacy.get(); + const index = config.kibana.index; + const collector = usageCollection.makeUsageCollector({ type: 'sample-data', fetch: fetchProvider(index), diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss index 79c3d4cca7ace..b8022201acf59 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss @@ -1,4 +1,3 @@ - .solutionToolbarButton { line-height: $euiButtonHeight; // Keeps alignment of text and chart icon background-color: $euiColorEmptyShade; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/button.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.tsx index 5de8e24ef5f0d..ee1bbd64b5f87 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/button.tsx +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.tsx @@ -12,17 +12,19 @@ import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/butt import './button.scss'; -export interface Props extends Pick { +export interface Props + extends Pick { label: string; primary?: boolean; + isDarkModeEnabled?: boolean; } -export const SolutionToolbarButton = ({ label, primary, ...rest }: Props) => ( +export const SolutionToolbarButton = ({ label, primary, className, ...rest }: Props) => ( {label} diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx index fbb34e165190d..33850005b498b 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx @@ -20,14 +20,20 @@ type AllowedPopoverProps = Omit< export type Props = AllowedButtonProps & AllowedPopoverProps; -export const SolutionToolbarPopover = ({ label, iconType, primary, ...popover }: Props) => { +export const SolutionToolbarPopover = ({ + label, + iconType, + primary, + iconSide, + ...popover +}: Props) => { const [isOpen, setIsOpen] = useState(false); const onButtonClick = () => setIsOpen((status) => !status); const closePopover = () => setIsOpen(false); const button = ( - + ); return ( diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.scss new file mode 100644 index 0000000000000..c3d89f430d70c --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.scss @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Temporary fix for lensApp icon not support ghost color +.solutionToolbar__primaryButton--dark { + .euiIcon path { + fill: $euiColorInk; + } +} + +.solutionToolbar__primaryButton--light { + .euiIcon path { + fill: $euiColorGhost; + } +} diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.tsx index e2ef75e45a404..dcf16228ac63b 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.tsx +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.tsx @@ -10,6 +10,20 @@ import React from 'react'; import { SolutionToolbarButton, Props as SolutionToolbarButtonProps } from './button'; -export const PrimaryActionButton = (props: Omit) => ( - +import './primary_button.scss'; + +export interface Props extends Omit { + isDarkModeEnabled?: boolean; +} + +export const PrimaryActionButton = ({ isDarkModeEnabled, ...props }: Props) => ( + ); diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss index 639ff5bf2a117..870a9a945ed5d 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss @@ -2,4 +2,17 @@ .quickButtonGroup__button { background-color: $euiColorEmptyShade; } + + // Temporary fix for two tone icons to make them monochrome + .quickButtonGroup__button--dark { + .euiIcon path { + fill: $euiColorGhost; + } + } + // Temporary fix for two tone icons to make them monochrome + .quickButtonGroup__button--light { + .euiIcon path { + fill: $euiColorInk; + } + } } diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx index 58f8bd803b636..eb0a395548cd9 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx @@ -17,23 +17,27 @@ import './quick_group.scss'; export interface QuickButtonProps extends Pick { createType: string; onClick: () => void; + isDarkModeEnabled?: boolean; } export interface Props { buttons: QuickButtonProps[]; } -type Option = EuiButtonGroupOptionProps & Omit; +type Option = EuiButtonGroupOptionProps & + Omit; export const QuickButtonGroup = ({ buttons }: Props) => { const buttonGroupOptions: Option[] = buttons.map((button: QuickButtonProps, index) => { - const { createType: label, ...rest } = button; + const { createType: label, isDarkModeEnabled, ...rest } = button; const title = strings.getAriaButtonLabel(label); return { ...rest, 'aria-label': title, - className: 'quickButtonGroup__button', + className: `quickButtonGroup__button ${ + isDarkModeEnabled ? 'quickButtonGroup__button--dark' : 'quickButtonGroup__button--light' + }`, id: `${htmlIdGenerator()()}${index}`, label, title, @@ -46,7 +50,7 @@ export const QuickButtonGroup = ({ buttons }: Props) => { return ( { +export const SolutionToolbar = ({ isDarkModeEnabled, children }: Props) => { const { primaryActionButton, quickButtonGroup, @@ -49,8 +50,10 @@ export const SolutionToolbar = ({ children }: Props) => { return ( {primaryActionButton} diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index 9c5f65de40955..fd3ae89419297 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -19,6 +19,7 @@ export { LazySavedObjectSaveModalDashboard, withSuspense, } from './components'; + export { AddFromLibraryButton, PrimaryActionButton, diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index 2b5a611cd946e..48bff8d203ebd 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -113,7 +113,7 @@ export class VisualizeEmbeddableFactory public getDisplayName() { return i18n.translate('visualizations.displayName', { - defaultMessage: 'visualization', + defaultMessage: 'Visualization', }); } diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index e5b1ba73d9d1c..dbcbb864d2316 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -25,7 +25,7 @@ export { getVisSchemas } from './vis_schemas'; /** @public types */ export { VisualizationsSetup, VisualizationsStart }; export { VisGroups } from './vis_types'; -export type { VisTypeAlias, VisTypeDefinition, Schema, ISchemas } from './vis_types'; +export type { BaseVisType, VisTypeAlias, VisTypeDefinition, Schema, ISchemas } from './vis_types'; export { SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; diff --git a/src/plugins/visualizations/public/wizard/dialog_navigation.tsx b/src/plugins/visualizations/public/wizard/dialog_navigation.tsx index 1de177e12f40d..c92514d54166f 100644 --- a/src/plugins/visualizations/public/wizard/dialog_navigation.tsx +++ b/src/plugins/visualizations/public/wizard/dialog_navigation.tsx @@ -24,7 +24,7 @@ function DialogNavigation(props: DialogNavigationProps) {
{i18n.translate('visualizations.newVisWizard.goBackLink', { - defaultMessage: 'Go back', + defaultMessage: 'Select a different visualization', })} diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx index d36b734f75be2..317f9d1bb363d 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx @@ -41,6 +41,8 @@ interface TypeSelectionProps { outsideVisualizeApp?: boolean; stateTransfer?: EmbeddableStateTransfer; originatingApp?: string; + showAggsSelection?: boolean; + selectedVisType?: BaseVisType; } interface TypeSelectionState { @@ -69,8 +71,9 @@ class NewVisModal extends React.Component import('./new_vis_modal')); @@ -29,6 +30,8 @@ export interface ShowNewVisModalParams { originatingApp?: string; outsideVisualizeApp?: boolean; createByValue?: boolean; + showAggsSelection?: boolean; + selectedVisType?: BaseVisType; } /** @@ -41,6 +44,8 @@ export function showNewVisModal({ onClose, originatingApp, outsideVisualizeApp, + showAggsSelection, + selectedVisType, }: ShowNewVisModalParams = {}) { const container = document.createElement('div'); let isClosed = false; @@ -78,6 +83,8 @@ export function showNewVisModal({ usageCollection={getUsageCollector()} application={getApplication()} docLinks={getDocLinks()} + showAggsSelection={showAggsSelection} + selectedVisType={selectedVisType} /> diff --git a/test/examples/embeddables/adding_children.ts b/test/examples/embeddables/adding_children.ts index 8b59012bf9825..ee06622a33f51 100644 --- a/test/examples/embeddables/adding_children.ts +++ b/test/examples/embeddables/adding_children.ts @@ -13,31 +13,12 @@ import { PluginFunctionalProviderContext } from 'test/plugin_functional/services export default function ({ getService }: PluginFunctionalProviderContext) { const testSubjects = getService('testSubjects'); const flyout = getService('flyout'); - const retry = getService('retry'); - describe('creating and adding children', () => { + describe('adding children', () => { before(async () => { await testSubjects.click('embeddablePanelExample'); }); - it('Can create a new child', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-ACTION_ADD_PANEL'); - - // this seem like an overkill, but clicking this button which opens context menu was flaky - await testSubjects.waitForEnabled('createNew'); - await retry.waitFor('createNew popover opened', async () => { - await testSubjects.click('createNew'); - return await testSubjects.exists('createNew-TODO_EMBEDDABLE'); - }); - await testSubjects.click('createNew-TODO_EMBEDDABLE'); - - await testSubjects.setValue('taskInputField', 'new task'); - await testSubjects.click('createTodoEmbeddable'); - const tasks = await testSubjects.getVisibleTextAll('todoEmbeddableTask'); - expect(tasks).to.eql(['Goes out on Wednesdays!', 'new task']); - }); - it('Can add a child backed off a saved object', async () => { await testSubjects.click('embeddablePanelToggleMenuIcon'); await testSubjects.click('embeddablePanelAction-ACTION_ADD_PANEL'); @@ -46,7 +27,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { await testSubjects.moveMouseTo('euiFlyoutCloseButton'); await flyout.ensureClosed('dashboardAddPanel'); const tasks = await testSubjects.getVisibleTextAll('todoEmbeddableTask'); - expect(tasks).to.eql(['Goes out on Wednesdays!', 'new task', 'Take the garbage out']); + expect(tasks).to.eql(['Goes out on Wednesdays!', 'Take the garbage out']); }); }); } diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.ts b/test/functional/apps/dashboard/create_and_add_embeddables.ts index 9b8fc4785a671..3de3b2f843f55 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.ts +++ b/test/functional/apps/dashboard/create_and_add_embeddables.ts @@ -35,8 +35,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('adds new visualization via the top nav link', async () => { const originalPanelCount = await PageObjects.dashboard.getPanelCount(); await PageObjects.dashboard.switchToEditMode(); - await dashboardAddPanel.clickCreateNewLink(); - await PageObjects.visualize.clickAggBasedVisualizations(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); await PageObjects.visualize.clickAreaChart(); await PageObjects.visualize.clickNewSearch(); await PageObjects.visualize.saveVisualizationExpectSuccess( @@ -52,9 +52,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('adds a new visualization', async () => { const originalPanelCount = await PageObjects.dashboard.getPanelCount(); - await dashboardAddPanel.ensureAddPanelIsShowing(); - await dashboardAddPanel.clickAddNewEmbeddableLink('visualization'); - await PageObjects.visualize.clickAggBasedVisualizations(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); await PageObjects.visualize.clickAreaChart(); await PageObjects.visualize.clickNewSearch(); await PageObjects.visualize.saveVisualizationExpectSuccess( @@ -71,7 +70,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('adds a markdown visualization via the quick button', async () => { const originalPanelCount = await PageObjects.dashboard.getPanelCount(); - await PageObjects.dashboard.clickMarkdownQuickButton(); + await dashboardAddPanel.clickMarkdownQuickButton(); await PageObjects.visualize.saveVisualizationExpectSuccess( 'visualization from markdown quick button', { redirectToOrigin: true } @@ -84,21 +83,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); }); - it('adds an input control visualization via the quick button', async () => { - const originalPanelCount = await PageObjects.dashboard.getPanelCount(); - await PageObjects.dashboard.clickInputControlsQuickButton(); - await PageObjects.visualize.saveVisualizationExpectSuccess( - 'visualization from input control quick button', - { redirectToOrigin: true } - ); - - await retry.try(async () => { - const panelCount = await PageObjects.dashboard.getPanelCount(); - expect(panelCount).to.eql(originalPanelCount + 1); - }); - await PageObjects.dashboard.waitForRenderComplete(); - }); - it('saves the listing page instead of the visualization to the app link', async () => { await PageObjects.header.clickVisualize(true); const currentUrl = await browser.getCurrentUrl(); diff --git a/test/functional/apps/dashboard/dashboard_unsaved_listing.ts b/test/functional/apps/dashboard/dashboard_unsaved_listing.ts index 233d2e91467fe..1cdc4bbff2c53 100644 --- a/test/functional/apps/dashboard/dashboard_unsaved_listing.ts +++ b/test/functional/apps/dashboard/dashboard_unsaved_listing.ts @@ -25,8 +25,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('dashboard unsaved listing', () => { const addSomePanels = async () => { // add an area chart by value - await dashboardAddPanel.clickCreateNewLink(); - await PageObjects.visualize.clickAggBasedVisualizations(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); await PageObjects.visualize.clickAreaChart(); await PageObjects.visualize.clickNewSearch(); await PageObjects.visualize.saveVisualizationAndReturn(); @@ -132,8 +132,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.switchToEditMode(); // add another panel so we can delete it later - await dashboardAddPanel.clickCreateNewLink(); - await PageObjects.visualize.clickAggBasedVisualizations(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); await PageObjects.visualize.clickAreaChart(); await PageObjects.visualize.clickNewSearch(); await PageObjects.visualize.saveVisualizationExpectSuccess('Wildvis', { diff --git a/test/functional/apps/dashboard/dashboard_unsaved_state.ts b/test/functional/apps/dashboard/dashboard_unsaved_state.ts index e6cc91880010a..fd203cd8c1356 100644 --- a/test/functional/apps/dashboard/dashboard_unsaved_state.ts +++ b/test/functional/apps/dashboard/dashboard_unsaved_state.ts @@ -41,8 +41,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('shows the unsaved changes badge after adding panels', async () => { await PageObjects.dashboard.switchToEditMode(); // add an area chart by value - await dashboardAddPanel.clickCreateNewLink(); - await PageObjects.visualize.clickAggBasedVisualizations(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); await PageObjects.visualize.clickAreaChart(); await PageObjects.visualize.clickNewSearch(); await PageObjects.visualize.saveVisualizationAndReturn(); diff --git a/test/functional/apps/dashboard/edit_embeddable_redirects.ts b/test/functional/apps/dashboard/edit_embeddable_redirects.ts index 8b7b98a59aa12..be540e18a503f 100644 --- a/test/functional/apps/dashboard/edit_embeddable_redirects.ts +++ b/test/functional/apps/dashboard/edit_embeddable_redirects.ts @@ -13,10 +13,9 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); const esArchiver = getService('esArchiver'); - const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); const dashboardPanelActions = getService('dashboardPanelActions'); - const dashboardVisualizations = getService('dashboardVisualizations'); + const dashboardAddPanel = getService('dashboardAddPanel'); describe('edit embeddable redirects', () => { before(async () => { @@ -88,10 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const newTitle = 'test create panel originatingApp'; await PageObjects.dashboard.loadSavedDashboard('few panels'); await PageObjects.dashboard.switchToEditMode(); - await testSubjects.exists('dashboardAddNewPanelButton'); - await testSubjects.click('dashboardAddNewPanelButton'); - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); - await PageObjects.visualize.clickMarkdownWidget(); + await dashboardAddPanel.clickMarkdownQuickButton(); await PageObjects.visualize.saveVisualizationExpectSuccess(newTitle, { saveAsNew: true, redirectToOrigin: false, diff --git a/test/functional/apps/dashboard/edit_visualizations.js b/test/functional/apps/dashboard/edit_visualizations.js index ce32f53587e74..b2f21aefcf79c 100644 --- a/test/functional/apps/dashboard/edit_visualizations.js +++ b/test/functional/apps/dashboard/edit_visualizations.js @@ -14,13 +14,14 @@ export default function ({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); const kibanaServer = getService('kibanaServer'); + const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); const originalMarkdownText = 'Original markdown text'; const modifiedMarkdownText = 'Modified markdown text'; const createMarkdownVis = async (title) => { - await PageObjects.dashboard.clickMarkdownQuickButton(); + await dashboardAddPanel.clickMarkdownQuickButton(); await PageObjects.visEditor.setMarkdownTxt(originalMarkdownText); await PageObjects.visEditor.clickGo(); if (title) { diff --git a/test/functional/apps/dashboard/empty_dashboard.ts b/test/functional/apps/dashboard/empty_dashboard.ts index c096d90aa3595..2cfa6d73dcb72 100644 --- a/test/functional/apps/dashboard/empty_dashboard.ts +++ b/test/functional/apps/dashboard/empty_dashboard.ts @@ -41,15 +41,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should open add panel when add button is clicked', async () => { - await testSubjects.click('dashboardAddPanelButton'); + await dashboardAddPanel.clickOpenAddPanel(); const isAddPanelOpen = await dashboardAddPanel.isAddPanelOpen(); expect(isAddPanelOpen).to.be(true); await testSubjects.click('euiFlyoutCloseButton'); }); it('should add new visualization from dashboard', async () => { - await testSubjects.exists('dashboardAddNewPanelButton'); - await testSubjects.click('dashboardAddNewPanelButton'); await dashboardVisualizations.createAndAddMarkdown({ name: 'Dashboard Test Markdown', markdown: 'Markdown text', @@ -57,5 +55,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); await dashboardExpect.markdownWithValuesExists(['Markdown text']); }); + + it('should open editor menu when editor button is clicked', async () => { + await dashboardAddPanel.clickEditorMenuButton(); + await testSubjects.existOrFail('dashboardEditorContextMenu'); + }); }); } diff --git a/test/functional/apps/dashboard/view_edit.ts b/test/functional/apps/dashboard/view_edit.ts index c5c7daab27ff1..99a78ebd069c5 100644 --- a/test/functional/apps/dashboard/view_edit.ts +++ b/test/functional/apps/dashboard/view_edit.ts @@ -113,10 +113,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('when a new vis is added', async function () { const originalPanelCount = await PageObjects.dashboard.getPanelCount(); - - await dashboardAddPanel.ensureAddPanelIsShowing(); - await dashboardAddPanel.clickAddNewEmbeddableLink('visualization'); - await PageObjects.visualize.clickAggBasedVisualizations(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); await PageObjects.visualize.clickAreaChart(); await PageObjects.visualize.clickNewSearch(); await PageObjects.visualize.saveVisualizationExpectSuccess('new viz panel', { diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 34559afdf6ae1..9c12296db138c 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -413,16 +413,6 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide await testSubjects.click('confirmSaveSavedObjectButton'); } - public async clickMarkdownQuickButton() { - log.debug('Click markdown quick button'); - await testSubjects.click('dashboardMarkdownQuickButton'); - } - - public async clickInputControlsQuickButton() { - log.debug('Click input controls quick button'); - await testSubjects.click('dashboardInputControlsQuickButton'); - } - /** * * @param dashboardTitle {String} diff --git a/test/functional/services/dashboard/add_panel.ts b/test/functional/services/dashboard/add_panel.ts index 7bb1603e0193f..a4e0c8b2647dd 100644 --- a/test/functional/services/dashboard/add_panel.ts +++ b/test/functional/services/dashboard/add_panel.ts @@ -30,15 +30,41 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }: FtrPro await PageObjects.common.sleep(500); } + async clickQuickButton(visType: string) { + log.debug(`DashboardAddPanel.clickQuickButton${visType}`); + await testSubjects.click(`dashboardQuickButton${visType}`); + } + + async clickMarkdownQuickButton() { + await this.clickQuickButton('markdown'); + } + + async clickMapQuickButton() { + await this.clickQuickButton('map'); + } + + async clickEditorMenuButton() { + log.debug('DashboardAddPanel.clickEditorMenuButton'); + await testSubjects.click('dashboardEditorMenuButton'); + } + + async clickAggBasedVisualizations() { + log.debug('DashboardAddPanel.clickEditorMenuAggBasedMenuItem'); + await testSubjects.click('dashboardEditorAggBasedMenuItem'); + } + async clickVisType(visType: string) { log.debug('DashboardAddPanel.clickVisType'); await testSubjects.click(`visType-${visType}`); } + async clickEmbeddableFactoryGroupButton(groupId: string) { + log.debug('DashboardAddPanel.clickEmbeddableFactoryGroupButton'); + await testSubjects.click(`dashboardEditorMenu-${groupId}Group`); + } + async clickAddNewEmbeddableLink(type: string) { - await testSubjects.click('createNew'); await testSubjects.click(`createNew-${type}`); - await testSubjects.missingOrFail(`createNew-${type}`); } async toggleFilterPopover() { diff --git a/test/functional/services/dashboard/visualizations.ts b/test/functional/services/dashboard/visualizations.ts index d1aaa6aa1bd70..2bf7458ff9c5f 100644 --- a/test/functional/services/dashboard/visualizations.ts +++ b/test/functional/services/dashboard/visualizations.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function DashboardVisualizationProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); - const find = getService('find'); - const retry = getService('retry'); const queryBar = getService('queryBar'); const testSubjects = getService('testSubjects'); const dashboardAddPanel = getService('dashboardAddPanel'); @@ -31,8 +29,8 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F if (inViewMode) { await PageObjects.dashboard.switchToEditMode(); } - await dashboardAddPanel.ensureAddPanelIsShowing(); - await dashboardAddPanel.clickAddNewEmbeddableLink('visualization'); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAddNewEmbeddableLink('metrics'); await PageObjects.visualize.clickVisualBuilder(); await PageObjects.visualize.saveVisualizationExpectSuccess(name); } @@ -87,39 +85,13 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F await dashboardAddPanel.addSavedSearch(name); } - async clickAddVisualizationButton() { - log.debug('DashboardVisualizations.clickAddVisualizationButton'); - await testSubjects.click('dashboardAddNewPanelButton'); - } - - async isNewVisDialogShowing() { - log.debug('DashboardVisualizations.isNewVisDialogShowing'); - return await find.existsByCssSelector('.visNewVisDialog'); - } - - async ensureNewVisualizationDialogIsShowing() { - let isShowing = await this.isNewVisDialogShowing(); - log.debug(`DashboardVisualizations.ensureNewVisualizationDialogIsShowing:${isShowing}`); - if (!isShowing) { - await retry.try(async () => { - await this.clickAddVisualizationButton(); - isShowing = await this.isNewVisDialogShowing(); - log.debug(`DashboardVisualizations.ensureNewVisualizationDialogIsShowing:${isShowing}`); - if (!isShowing) { - throw new Error('New Vis Dialog still not open, trying again.'); - } - }); - } - } - async createAndAddMarkdown({ name, markdown }: { name: string; markdown: string }) { log.debug(`createAndAddMarkdown(${markdown})`); const inViewMode = await PageObjects.dashboard.getIsInViewMode(); if (inViewMode) { await PageObjects.dashboard.switchToEditMode(); } - await this.ensureNewVisualizationDialogIsShowing(); - await PageObjects.visualize.clickMarkdownWidget(); + await dashboardAddPanel.clickMarkdownQuickButton(); await PageObjects.visEditor.setMarkdownTxt(markdown); await PageObjects.visEditor.clickGo(); await PageObjects.visualize.saveVisualizationExpectSuccess(name, { @@ -134,10 +106,10 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F if (inViewMode) { await PageObjects.dashboard.switchToEditMode(); } - await this.ensureNewVisualizationDialogIsShowing(); - await PageObjects.visualize.clickAggBasedVisualizations(); - await PageObjects.visualize.clickMetric(); - await find.clickByCssSelector('li.euiListGroupItem:nth-of-type(2)'); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); + await dashboardAddPanel.clickVisType('metric'); + await testSubjects.click('savedObjectTitlelogstash-*'); await testSubjects.exists('visualizesaveAndReturnButton'); await testSubjects.click('visualizesaveAndReturnButton'); } @@ -148,8 +120,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F if (inViewMode) { await PageObjects.dashboard.switchToEditMode(); } - await this.ensureNewVisualizationDialogIsShowing(); - await PageObjects.visualize.clickMarkdownWidget(); + await dashboardAddPanel.clickMarkdownQuickButton(); await PageObjects.visEditor.setMarkdownTxt(markdown); await PageObjects.visEditor.clickGo(); await testSubjects.click('visualizesaveAndReturnButton'); diff --git a/test/new_visualize_flow/dashboard_embedding.ts b/test/new_visualize_flow/dashboard_embedding.ts index 6a1315dbfc91e..04b91542223ba 100644 --- a/test/new_visualize_flow/dashboard_embedding.ts +++ b/test/new_visualize_flow/dashboard_embedding.ts @@ -22,7 +22,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const dashboardExpect = getService('dashboardExpect'); - const testSubjects = getService('testSubjects'); const dashboardVisualizations = getService('dashboardVisualizations'); const PageObjects = getPageObjects([ 'common', @@ -47,8 +46,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('adding a metric visualization', async function () { const originalPanelCount = await PageObjects.dashboard.getPanelCount(); expect(originalPanelCount).to.eql(0); - await testSubjects.exists('dashboardAddNewPanelButton'); - await testSubjects.click('dashboardAddNewPanelButton'); await dashboardVisualizations.createAndEmbedMetric('Embedding Vis Test'); await PageObjects.dashboard.waitForRenderComplete(); await dashboardExpect.metricValuesExist(['0']); @@ -59,8 +56,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('adding a markdown', async function () { const originalPanelCount = await PageObjects.dashboard.getPanelCount(); expect(originalPanelCount).to.eql(1); - await testSubjects.exists('dashboardAddNewPanelButton'); - await testSubjects.click('dashboardAddNewPanelButton'); await dashboardVisualizations.createAndEmbedMarkdown({ name: 'Embedding Markdown Test', markdown: 'Nice to meet you, markdown is my name', diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index 62bd07ce6f500..12df93d54b296 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -6,9 +6,9 @@ */ import { i18n } from '@kbn/i18n'; -import { ValuesType } from 'utility-types'; -import { ActionGroup } from '../../alerting/common'; -import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../../ml/common'; +import type { ValuesType } from 'utility-types'; +import type { ActionGroup } from '../../alerting/common'; +import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from './ml_constants'; export enum AlertType { ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat. diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index b9cc3de8bb5d0..43a779407d2a4 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ANOMALY_SEVERITY } from '../../ml/common'; +import { ANOMALY_SEVERITY } from './ml_constants'; import { getSeverityType, getSeverityColor as mlGetSeverityColor, diff --git a/x-pack/plugins/apm/common/ml_constants.ts b/x-pack/plugins/apm/common/ml_constants.ts new file mode 100644 index 0000000000000..7818299d9d883 --- /dev/null +++ b/x-pack/plugins/apm/common/ml_constants.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +// copied from ml/common, to keep the bundle size small +export enum ANOMALY_SEVERITY { + CRITICAL = 'critical', + MAJOR = 'major', + MINOR = 'minor', + WARNING = 'warning', + LOW = 'low', + UNKNOWN = 'unknown', +} + +export enum ANOMALY_THRESHOLD { + CRITICAL = 75, + MAJOR = 50, + MINOR = 25, + WARNING = 3, + LOW = 0, +} diff --git a/x-pack/plugins/apm/common/rules.ts b/x-pack/plugins/apm/common/rules.ts deleted file mode 100644 index a3b60a785f5c7..0000000000000 --- a/x-pack/plugins/apm/common/rules.ts +++ /dev/null @@ -1,25 +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. - */ - -const plainApmRuleRegistrySettings = { - name: 'apm', - fieldMap: { - 'service.environment': { - type: 'keyword', - }, - 'transaction.type': { - type: 'keyword', - }, - 'processor.event': { - type: 'keyword', - }, - }, -} as const; - -type APMRuleRegistrySettings = typeof plainApmRuleRegistrySettings; - -export const apmRuleRegistrySettings: APMRuleRegistrySettings = plainApmRuleRegistrySettings; diff --git a/x-pack/plugins/apm/common/rules/apm_rule_field_map.ts b/x-pack/plugins/apm/common/rules/apm_rule_field_map.ts new file mode 100644 index 0000000000000..9bbd9381c2319 --- /dev/null +++ b/x-pack/plugins/apm/common/rules/apm_rule_field_map.ts @@ -0,0 +1,20 @@ +/* + * 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 const apmRuleFieldMap = { + 'service.environment': { + type: 'keyword', + }, + 'transaction.type': { + type: 'keyword', + }, + 'processor.event': { + type: 'keyword', + }, +} as const; + +export type APMRuleFieldMap = typeof apmRuleFieldMap; diff --git a/x-pack/plugins/apm/common/rules/apm_rule_registry_settings.ts b/x-pack/plugins/apm/common/rules/apm_rule_registry_settings.ts new file mode 100644 index 0000000000000..1257db4e6a4d3 --- /dev/null +++ b/x-pack/plugins/apm/common/rules/apm_rule_registry_settings.ts @@ -0,0 +1,10 @@ +/* + * 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 const apmRuleRegistrySettings = { + name: 'apm', +}; diff --git a/x-pack/plugins/apm/common/service_health_status.ts b/x-pack/plugins/apm/common/service_health_status.ts index 71c373a48c9d5..b5318f9333e4f 100644 --- a/x-pack/plugins/apm/common/service_health_status.ts +++ b/x-pack/plugins/apm/common/service_health_status.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { EuiTheme } from '../../../../src/plugins/kibana_react/common'; -import { ANOMALY_SEVERITY } from '../../ml/common'; +import { ANOMALY_SEVERITY } from './ml_constants'; export enum ServiceHealthStatus { healthy = 'healthy', diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 8834cbc70e0b1..583be94c30a34 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -7,11 +7,20 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; -import { format } from 'url'; +import { stringify } from 'querystring'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; -import { asDuration, asPercent } from '../../../common/utils/formatters'; import { AlertType } from '../../../common/alert_types'; -import { ApmRuleRegistry } from '../../plugin'; +import type { ApmRuleRegistry } from '../../plugin'; + +const format = ({ + pathname, + query, +}: { + pathname: string; + query: Record; +}): string => { + return `${pathname}?${stringify(query)}`; +}; export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { apmRuleRegistry.registerType({ @@ -71,7 +80,7 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { 'Alert when the latency of a specific transaction type in a service exceeds a defined threshold.', } ), - format: ({ alert }) => ({ + format: ({ alert, formatters: { asDuration } }) => ({ reason: i18n.translate( 'xpack.apm.alertTypes.transactionDuration.reason', { @@ -131,7 +140,7 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { 'Alert when the rate of transaction errors in a service exceeds a defined threshold.', } ), - format: ({ alert }) => ({ + format: ({ alert, formatters: { asPercent } }) => ({ reason: i18n.translate( 'xpack.apm.alertTypes.transactionErrorRate.reason', { diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx index 62926796cafb4..10d139f6ccea3 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx @@ -8,7 +8,7 @@ import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { ANOMALY_SEVERITY } from '../../../../../ml/common'; +import { ANOMALY_SEVERITY } from '../../../../common/ml_constants'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { ServiceAlertTrigger } from '../service_alert_trigger'; diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx index 85f48ae151e10..7b56eaa4721de 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx @@ -8,7 +8,7 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { IntlProvider } from 'react-intl'; -import { ANOMALY_SEVERITY } from '../../../../../ml/common'; +import { ANOMALY_SEVERITY } from '../../../../common/ml_constants'; import { SelectAnomalySeverity } from './select_anomaly_severity'; function Wrapper({ children }: { children?: ReactNode }) { diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 391c54c1e2497..143076e56c831 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -5,13 +5,7 @@ * 2.0. */ -import { ConfigSchema } from '.'; -import { - FetchDataParams, - FormatterRuleRegistry, - HasDataParams, - ObservabilityPublicSetup, -} from '../../observability/public'; +import type { ConfigSchema } from '.'; import { AppMountParameters, CoreSetup, @@ -20,28 +14,35 @@ import { Plugin, PluginInitializerContext, } from '../../../../src/core/public'; -import { +import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../src/plugins/data/public'; -import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; -import { +import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import type { PluginSetupContract as AlertingPluginPublicSetup, PluginStartContract as AlertingPluginPublicStart, } from '../../alerting/public'; -import { FeaturesPluginSetup } from '../../features/public'; -import { LicensingPluginSetup } from '../../licensing/public'; -import { +import type { FeaturesPluginSetup } from '../../features/public'; +import type { LicensingPluginSetup } from '../../licensing/public'; +import type { MapsStartApi } from '../../maps/public'; +import type { MlPluginSetup, MlPluginStart } from '../../ml/public'; +import type { + FetchDataParams, + HasDataParams, + ObservabilityPublicSetup, +} from '../../observability/public'; +import { FormatterRuleRegistry } from '../../observability/public'; +import type { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; +import { apmRuleRegistrySettings } from '../common/rules/apm_rule_registry_settings'; +import type { APMRuleFieldMap } from '../common/rules/apm_rule_field_map'; +import { registerApmAlerts } from './components/alerting/register_apm_alerts'; import { featureCatalogueEntry } from './featureCatalogueEntry'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; -import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; -import { registerApmAlerts } from './components/alerting/register_apm_alerts'; -import { MlPluginSetup, MlPluginStart } from '../../ml/public'; -import { MapsStartApi } from '../../maps/public'; -import { apmRuleRegistrySettings } from '../common/rules'; export type ApmPluginSetup = ReturnType; export type ApmRuleRegistry = ApmPluginSetup['ruleRegistry']; @@ -162,6 +163,7 @@ export class ApmPlugin implements Plugin { const apmRuleRegistry = plugins.observability.ruleRegistry.create({ ...apmRuleRegistrySettings, + fieldMap: {} as APMRuleFieldMap, ctor: FormatterRuleRegistry, }); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.test.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.test.ts index b9346b2bf4649..ad1a8fcbf6e55 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.test.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.test.ts @@ -5,7 +5,7 @@ * 2.0. */ import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type'; -import { ANOMALY_SEVERITY } from '../../../../ml/common'; +import { ANOMALY_SEVERITY } from '../../../common/ml_constants'; import { Job, MlPluginSetup } from '../../../../ml/server'; import * as GetServiceAnomalies from '../service_map/get_service_anomalies'; import { createRuleTypeMocks } from './test_utils'; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 66eb7125b0370..67ff7cdb8e4e0 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -18,7 +18,7 @@ import { TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; -import { ANOMALY_SEVERITY } from '../../../../ml/common'; +import { ANOMALY_SEVERITY } from '../../../common/ml_constants'; import { KibanaRequest } from '../../../../../../src/core/server'; import { AlertType, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts index a03b1ac82e90a..bcd279c57f4a5 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts @@ -14,7 +14,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { anomalySeriesFetcher } from './fetcher'; import { getMLJobIds } from '../../service_map/get_service_anomalies'; -import { ANOMALY_THRESHOLD } from '../../../../../ml/common'; +import { ANOMALY_THRESHOLD } from '../../../../common/ml_constants'; import { withApmSpan } from '../../../utils/with_apm_span'; export async function getAnomalySeries({ diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 714b887a4008b..d62a3e6a5d5d7 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -42,7 +42,8 @@ import { } from './types'; import { registerRoutes } from './routes/register_routes'; import { getGlobalApmServerRouteRepository } from './routes/get_global_apm_server_route_repository'; -import { apmRuleRegistrySettings } from '../common/rules'; +import { apmRuleRegistrySettings } from '../common/rules/apm_rule_registry_settings'; +import { apmRuleFieldMap } from '../common/rules/apm_rule_field_map'; export type APMRuleRegistry = ReturnType['ruleRegistry']; @@ -151,9 +152,10 @@ export class APMPlugin config: await mergedConfig$.pipe(take(1)).toPromise(), }); - const apmRuleRegistry = plugins.observability.ruleRegistry.create( - apmRuleRegistrySettings - ); + const apmRuleRegistry = plugins.observability.ruleRegistry.create({ + ...apmRuleRegistrySettings, + fieldMap: apmRuleFieldMap, + }); registerApmAlerts({ registry: apmRuleRegistry, diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 3bc0d97d64646..1a594e77f4857 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -30,7 +30,7 @@ export enum InstallStatus { uninstalling = 'uninstalling', } -export type InstallType = 'reinstall' | 'reupdate' | 'rollback' | 'update' | 'install'; +export type InstallType = 'reinstall' | 'reupdate' | 'rollback' | 'update' | 'install' | 'unknown'; export type InstallSource = 'registry' | 'upload'; export type EpmPackageInstallStatus = 'installed' | 'installing'; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts index 3c7a32265d20a..e5c7ace420c73 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts @@ -12,6 +12,7 @@ import type { RegistrySearchResult, PackageInfo, PackageUsageStats, + InstallType, } from '../models/epm'; export interface GetCategoriesRequest { @@ -83,8 +84,10 @@ export interface IBulkInstallPackageHTTPError { } export interface InstallResult { - assets: AssetReference[]; - status: 'installed' | 'already_installed'; + assets?: AssetReference[]; + status?: 'installed' | 'already_installed'; + error?: Error; + installType: InstallType; } export interface BulkInstallPackageInfo { diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index f0d6e68427361..16d583f8a8d1f 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -226,20 +226,21 @@ export const installPackageFromRegistryHandler: RequestHandler< const savedObjectsClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; const { pkgkey } = request.params; - try { - const res = await installPackage({ - installSource: 'registry', - savedObjectsClient, - pkgkey, - esClient, - force: request.body?.force, - }); + + const res = await installPackage({ + installSource: 'registry', + savedObjectsClient, + pkgkey, + esClient, + force: request.body?.force, + }); + if (!res.error) { const body: InstallPackageResponse = { - response: res.assets, + response: res.assets || [], }; return response.ok({ body }); - } catch (e) { - return await defaultIngestErrorHandler({ error: e, response }); + } else { + return await defaultIngestErrorHandler({ error: res.error, response }); } }; @@ -292,20 +293,21 @@ export const installPackageByUploadHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; const contentType = request.headers['content-type'] as string; // from types it could also be string[] or undefined but this is checked later const archiveBuffer = Buffer.from(request.body); - try { - const res = await installPackage({ - installSource: 'upload', - savedObjectsClient, - esClient, - archiveBuffer, - contentType, - }); + + const res = await installPackage({ + installSource: 'upload', + savedObjectsClient, + esClient, + archiveBuffer, + contentType, + }); + if (!res.error) { const body: InstallPackageResponse = { - response: res.assets, + response: res.assets || [], }; return response.ok({ body }); - } catch (error) { - return defaultIngestErrorHandler({ error, response }); + } else { + return defaultIngestErrorHandler({ error: res.error, response }); } }; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts index 7323263d4a70f..baaaaf6c6b0cf 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts @@ -32,22 +32,27 @@ export async function bulkInstallPackages({ ); logger.debug(`kicking off bulk install of ${packagesToInstall.join(', ')} from registry`); - const installResults = await Promise.allSettled( + const bulkInstallResults = await Promise.allSettled( latestPackagesResults.map(async (result, index) => { const packageName = packagesToInstall[index]; if (result.status === 'fulfilled') { const latestPackage = result.value; - return { - name: packageName, - version: latestPackage.version, - result: await installPackage({ - savedObjectsClient, - esClient, - pkgkey: Registry.pkgToPkgKey(latestPackage), - installSource, - skipPostInstall: true, - }), - }; + const installResult = await installPackage({ + savedObjectsClient, + esClient, + pkgkey: Registry.pkgToPkgKey(latestPackage), + installSource, + skipPostInstall: true, + }); + if (installResult.error) { + return { name: packageName, error: installResult.error }; + } else { + return { + name: packageName, + version: latestPackage.version, + result: installResult, + }; + } } return { name: packageName, error: result.reason }; }) @@ -56,18 +61,27 @@ export async function bulkInstallPackages({ // only install index patterns if we completed install for any package-version for the // first time, aka fresh installs or upgrades if ( - installResults.find( - (result) => result.status === 'fulfilled' && result.value.result?.status === 'installed' + bulkInstallResults.find( + (result) => + result.status === 'fulfilled' && + !result.value.result?.error && + result.value.result?.status === 'installed' ) ) { await installIndexPatterns({ savedObjectsClient, esClient, installSource }); } - return installResults.map((result, index) => { + return bulkInstallResults.map((result, index) => { const packageName = packagesToInstall[index]; - return result.status === 'fulfilled' - ? result.value - : { name: packageName, error: result.reason }; + if (result.status === 'fulfilled') { + if (result.value && result.value.error) { + return { name: packageName, error: result.value.error }; + } else { + return result.value; + } + } else { + return { name: packageName, error: result.reason }; + } }); } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts index fa2ea9e2209ed..f8c91e55fbbb6 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts @@ -77,7 +77,7 @@ describe('ensureInstalledDefaultPackages', () => { return [ { name: mockInstallation.attributes.name, - result: { assets: [], status: 'installed' }, + result: { assets: [], status: 'installed', installType: 'install' }, version: '', statusCode: 200, }, @@ -95,13 +95,13 @@ describe('ensureInstalledDefaultPackages', () => { return [ { name: 'success one', - result: { assets: [], status: 'installed' }, + result: { assets: [], status: 'installed', installType: 'install' }, version: '', statusCode: 200, }, { name: 'success two', - result: { assets: [], status: 'installed' }, + result: { assets: [], status: 'installed', installType: 'install' }, version: '', statusCode: 200, }, @@ -111,7 +111,7 @@ describe('ensureInstalledDefaultPackages', () => { }, { name: 'success three', - result: { assets: [], status: 'installed' }, + result: { assets: [], status: 'installed', installType: 'install' }, version: '', statusCode: 200, }, @@ -134,7 +134,7 @@ describe('ensureInstalledDefaultPackages', () => { return [ { name: 'undefined package', - result: { assets: [], status: 'installed' }, + result: { assets: [], status: 'installed', installType: 'install' }, version: '', statusCode: 200, }, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 4373251a969bc..31d0732096790 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -201,54 +201,62 @@ async function installPackageFromRegistry({ // TODO: change epm API to /packageName/version so we don't need to do this const { pkgName, pkgVersion } = Registry.splitPkgKey(pkgkey); - // get the currently installed package - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installType = getInstallType({ pkgVersion, installedPkg }); - - // get latest package version - const latestPackage = await Registry.fetchFindLatestPackage(pkgName); - - // let the user install if using the force flag or needing to reinstall or install a previous version due to failed update - const installOutOfDateVersionOk = - force || ['reinstall', 'reupdate', 'rollback'].includes(installType); + // if an error happens during getInstallType, report that we don't know + let installType: InstallType = 'unknown'; - // if the requested version is the same as installed version, check if we allow it based on - // current installed package status and force flag, if we don't allow it, - // just return the asset references from the existing installation - if ( - installedPkg?.attributes.version === pkgVersion && - installedPkg?.attributes.install_status === 'installed' - ) { - if (!force) { - logger.debug(`${pkgkey} is already installed, skipping installation`); - return { - assets: [ - ...installedPkg.attributes.installed_es, - ...installedPkg.attributes.installed_kibana, - ], - status: 'already_installed', - }; + try { + // get the currently installed package + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + installType = getInstallType({ pkgVersion, installedPkg }); + + // get latest package version + const latestPackage = await Registry.fetchFindLatestPackage(pkgName); + + // let the user install if using the force flag or needing to reinstall or install a previous version due to failed update + const installOutOfDateVersionOk = + force || ['reinstall', 'reupdate', 'rollback'].includes(installType); + + // if the requested version is the same as installed version, check if we allow it based on + // current installed package status and force flag, if we don't allow it, + // just return the asset references from the existing installation + if ( + installedPkg?.attributes.version === pkgVersion && + installedPkg?.attributes.install_status === 'installed' + ) { + if (!force) { + logger.debug(`${pkgkey} is already installed, skipping installation`); + return { + assets: [ + ...installedPkg.attributes.installed_es, + ...installedPkg.attributes.installed_kibana, + ], + status: 'already_installed', + installType, + }; + } } - } - // if the requested version is out-of-date of the latest package version, check if we allow it - // if we don't allow it, return an error - if (semverLt(pkgVersion, latestPackage.version)) { - if (!installOutOfDateVersionOk) { - throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); + // if the requested version is out-of-date of the latest package version, check if we allow it + // if we don't allow it, return an error + if (semverLt(pkgVersion, latestPackage.version)) { + if (!installOutOfDateVersionOk) { + throw new PackageOutdatedError( + `${pkgkey} is out-of-date and cannot be installed or updated` + ); + } + logger.debug( + `${pkgkey} is out-of-date, installing anyway due to ${ + force ? 'force flag' : `install type ${installType}` + }` + ); } - logger.debug( - `${pkgkey} is out-of-date, installing anyway due to ${ - force ? 'force flag' : `install type ${installType}` - }` - ); - } - // get package info - const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); + // get package info + const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); - // try installing the package, if there was an error, call error handler and rethrow - try { + // try installing the package, if there was an error, call error handler and rethrow + // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status + // @ts-ignore return _installPackage({ savedObjectsClient, esClient, @@ -257,19 +265,26 @@ async function installPackageFromRegistry({ packageInfo, installType, installSource: 'registry', - }).then((assets) => { - return { assets, status: 'installed' }; - }); + }) + .then((assets) => { + return { assets, status: 'installed', installType }; + }) + .catch(async (err: Error) => { + await handleInstallPackageFailure({ + savedObjectsClient, + error: err, + pkgName, + pkgVersion, + installedPkg, + esClient, + }); + return { error: err, installType }; + }); } catch (e) { - await handleInstallPackageFailure({ - savedObjectsClient, + return { error: e, - pkgName, - pkgVersion, - installedPkg, - esClient, - }); - throw e; + installType, + }; } } @@ -286,46 +301,57 @@ async function installPackageByUpload({ archiveBuffer, contentType, }: InstallUploadedArchiveParams): Promise { - const { packageInfo } = await parseAndVerifyArchiveEntries(archiveBuffer, contentType); - - const installedPkg = await getInstallationObject({ - savedObjectsClient, - pkgName: packageInfo.name, - }); + // if an error happens during getInstallType, report that we don't know + let installType: InstallType = 'unknown'; + try { + const { packageInfo } = await parseAndVerifyArchiveEntries(archiveBuffer, contentType); - const installType = getInstallType({ pkgVersion: packageInfo.version, installedPkg }); - if (installType !== 'install') { - throw new PackageOperationNotSupportedError( - `Package upload only supports fresh installations. Package ${packageInfo.name} is already installed, please uninstall first.` - ); - } + const installedPkg = await getInstallationObject({ + savedObjectsClient, + pkgName: packageInfo.name, + }); - const installSource = 'upload'; - const paths = await unpackBufferToCache({ - name: packageInfo.name, - version: packageInfo.version, - installSource, - archiveBuffer, - contentType, - }); + installType = getInstallType({ pkgVersion: packageInfo.version, installedPkg }); + if (installType !== 'install') { + throw new PackageOperationNotSupportedError( + `Package upload only supports fresh installations. Package ${packageInfo.name} is already installed, please uninstall first.` + ); + } - setPackageInfo({ - name: packageInfo.name, - version: packageInfo.version, - packageInfo, - }); + const installSource = 'upload'; + const paths = await unpackBufferToCache({ + name: packageInfo.name, + version: packageInfo.version, + installSource, + archiveBuffer, + contentType, + }); - return _installPackage({ - savedObjectsClient, - esClient, - installedPkg, - paths, - packageInfo, - installType, - installSource, - }).then((assets) => { - return { assets, status: 'installed' }; - }); + setPackageInfo({ + name: packageInfo.name, + version: packageInfo.version, + packageInfo, + }); + // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status + // @ts-ignore + return _installPackage({ + savedObjectsClient, + esClient, + installedPkg, + paths, + packageInfo, + installType, + installSource, + }) + .then((assets) => { + return { assets, status: 'installed', installType }; + }) + .catch(async (err: Error) => { + return { error: err, installType }; + }); + } catch (e) { + return { error: e, installType }; + } } export type InstallPackageParams = { @@ -352,7 +378,7 @@ export async function installPackage(args: InstallPackageParams) { esClient, force, }).then(async (installResult) => { - if (skipPostInstall) { + if (skipPostInstall || installResult.error) { return installResult; } logger.debug(`install of ${pkgkey} finished, running post-install`); @@ -374,7 +400,7 @@ export async function installPackage(args: InstallPackageParams) { archiveBuffer, contentType, }).then(async (installResult) => { - if (skipPostInstall) { + if (skipPostInstall || installResult.error) { return installResult; } logger.debug(`install of uploaded package finished, running post-install`); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index de798e822b029..706f1bbbaaf35 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -79,6 +79,7 @@ export async function removeInstallation(options: { return installedAssets; } +// TODO: this is very much like deleteKibanaSavedObjectsAssets below function deleteKibanaAssets( installedObjects: KibanaAssetReference[], savedObjectsClient: SavedObjectsClientContract @@ -136,6 +137,7 @@ async function deleteTemplate(esClient: ElasticsearchClient, name: string): Prom } } +// TODO: this is very much like deleteKibanaAssets above export async function deleteKibanaSavedObjectsAssets( savedObjectsClient: SavedObjectsClientContract, installedRefs: AssetReference[] @@ -153,6 +155,9 @@ export async function deleteKibanaSavedObjectsAssets( try { await Promise.all(deletePromises); } catch (err) { - logger.warn(err); + // in the rollback case, partial installs are likely, so missing assets are not an error + if (!savedObjectsClient.errors.isNotFoundError(err)) { + logger.error(err); + } } } diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts index 4b9b2f99215b7..1c7e8ceb28fb4 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts @@ -40,6 +40,16 @@ export class LogStreamEmbeddableFactoryDefinition }); } + public getDescription() { + return i18n.translate('xpack.infra.logStreamEmbeddable.description', { + defaultMessage: 'Add a table of live streaming logs.', + }); + } + + public getIconType() { + return 'logsApp'; + } + public async getExplicitInput() { return { title: i18n.translate('xpack.infra.logStreamEmbeddable.title', { diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 2915eaec8ac77..50043772af95b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -167,12 +167,6 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); - const requestResponder = this.getInspectorAdapters()?.requests?.start(requestName, { - id: requestId, - description: requestDescription, - searchSessionId, - }); - let resp; try { resp = await searchSource @@ -180,7 +174,12 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource abortSignal: abortController.signal, sessionId: searchSessionId, legacyHitsTotal: false, - requestResponder, + inspector: { + adapter: this.getInspectorAdapters()?.requests, + id: requestId, + title: requestName, + description: requestDescription, + }, }) .toPromise(); } catch (error) { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts index ac5ff2094e22b..4788d809f016f 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import type { StartServicesAccessor } from 'kibana/public'; +import { PLUGIN_ICON, PLUGIN_ID, ML_APP_NAME } from '../../../common/constants/app'; import type { EmbeddableFactoryDefinition, IContainer, @@ -27,6 +28,14 @@ export class AnomalyChartsEmbeddableFactory implements EmbeddableFactoryDefinition { public readonly type = ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE; + public readonly grouping = [ + { + id: PLUGIN_ID, + getDisplayName: () => ML_APP_NAME, + getIconType: () => PLUGIN_ICON, + }, + ]; + constructor( private getStartServices: StartServicesAccessor ) {} @@ -37,7 +46,13 @@ export class AnomalyChartsEmbeddableFactory public getDisplayName() { return i18n.translate('xpack.ml.components.mlAnomalyExplorerEmbeddable.displayName', { - defaultMessage: 'ML anomaly chart', + defaultMessage: 'Anomaly chart', + }); + } + + public getDescription() { + return i18n.translate('xpack.ml.components.mlAnomalyExplorerEmbeddable.description', { + defaultMessage: 'View anomaly detection results in a chart.', }); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts index fdb2ef8527923..bc45e075710c5 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import type { StartServicesAccessor } from 'kibana/public'; +import { PLUGIN_ID, PLUGIN_ICON, ML_APP_NAME } from '../../../common/constants/app'; import type { EmbeddableFactoryDefinition, IContainer, @@ -26,6 +27,14 @@ export class AnomalySwimlaneEmbeddableFactory implements EmbeddableFactoryDefinition { public readonly type = ANOMALY_SWIMLANE_EMBEDDABLE_TYPE; + public readonly grouping = [ + { + id: PLUGIN_ID, + getDisplayName: () => ML_APP_NAME, + getIconType: () => PLUGIN_ICON, + }, + ]; + constructor( private getStartServices: StartServicesAccessor ) {} @@ -36,7 +45,13 @@ export class AnomalySwimlaneEmbeddableFactory public getDisplayName() { return i18n.translate('xpack.ml.components.jobAnomalyScoreEmbeddable.displayName', { - defaultMessage: 'ML anomaly swim lane', + defaultMessage: 'Anomaly swim lane', + }); + } + + public getDescription() { + return i18n.translate('xpack.ml.components.jobAnomalyScoreEmbeddable.description', { + defaultMessage: 'View anomaly detection results in a timeline.', }); } diff --git a/x-pack/plugins/observability/common/observability_rule_registry.ts b/x-pack/plugins/observability/common/observability_rule_registry.ts deleted file mode 100644 index 9254401fc19c4..0000000000000 --- a/x-pack/plugins/observability/common/observability_rule_registry.ts +++ /dev/null @@ -1,22 +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 { ecsFieldMap, pickWithPatterns } from '../../rule_registry/common'; - -export const observabilityRuleRegistrySettings = { - name: 'observability', - fieldMap: { - ...pickWithPatterns(ecsFieldMap, 'host.name', 'service.name'), - 'kibana.observability.evaluation.value': { - type: 'scaled_float' as const, - scaling_factor: 1000, - }, - 'kibana.observability.evaluation.threshold': { - type: 'scaled_float' as const, - scaling_factor: 1000, - }, - }, -}; diff --git a/x-pack/plugins/observability/common/rules/observability_rule_field_map.ts b/x-pack/plugins/observability/common/rules/observability_rule_field_map.ts new file mode 100644 index 0000000000000..370f5d4ef79f2 --- /dev/null +++ b/x-pack/plugins/observability/common/rules/observability_rule_field_map.ts @@ -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 { ecsFieldMap, pickWithPatterns } from '../../../rule_registry/common'; + +export const observabilityRuleFieldMap = { + ...pickWithPatterns(ecsFieldMap, 'host.name', 'service.name'), + 'kibana.observability.evaluation.value': { + type: 'scaled_float' as const, + scaling_factor: 1000, + }, + 'kibana.observability.evaluation.threshold': { + type: 'scaled_float' as const, + scaling_factor: 1000, + }, +}; + +export type ObservabilityRuleFieldMap = typeof observabilityRuleFieldMap; diff --git a/x-pack/plugins/observability/common/rules/observability_rule_registry_settings.ts b/x-pack/plugins/observability/common/rules/observability_rule_registry_settings.ts new file mode 100644 index 0000000000000..c901d912eb70f --- /dev/null +++ b/x-pack/plugins/observability/common/rules/observability_rule_registry_settings.ts @@ -0,0 +1,10 @@ +/* + * 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 const observabilityRuleRegistrySettings = { + name: 'observability', +}; diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/index.tsx index 0089465003393..aa5fb2c32ea11 100644 --- a/x-pack/plugins/observability/public/pages/alerts/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/index.tsx @@ -24,6 +24,7 @@ import { usePluginContext } from '../../hooks/use_plugin_context'; import { RouteParams } from '../../routes'; import { callObservabilityApi } from '../../services/call_observability_api'; import { getAbsoluteDateRange } from '../../utils/date'; +import { asDuration, asPercent } from '../../../common/utils/formatters'; import { AlertsSearchBar } from './alerts_search_bar'; import { AlertsTable } from './alerts_table'; @@ -68,7 +69,7 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { const formatted = { link: undefined, reason: alert['rule.name'], - ...(ruleType?.format?.({ alert }) ?? {}), + ...(ruleType?.format?.({ alert, formatters: { asDuration, asPercent } }) ?? {}), }; const parsedLink = formatted.link ? parse(formatted.link, true) : undefined; diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 491eb36d01ac0..1f56bdebbbb9b 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -5,32 +5,33 @@ * 2.0. */ -import { BehaviorSubject } from 'rxjs'; import { i18n } from '@kbn/i18n'; -import type { RuleRegistryPublicPluginSetupContract } from '../../rule_registry/public'; -import type { - DataPublicPluginSetup, - DataPublicPluginStart, -} from '../../../../src/plugins/data/public'; +import { BehaviorSubject } from 'rxjs'; import { AppMountParameters, AppUpdater, CoreSetup, + CoreStart, DEFAULT_APP_CATEGORIES, Plugin as PluginClass, PluginInitializerContext, - CoreStart, } from '../../../../src/core/public'; +import type { + DataPublicPluginSetup, + DataPublicPluginStart, +} from '../../../../src/plugins/data/public'; import type { HomePublicPluginSetup, HomePublicPluginStart, } from '../../../../src/plugins/home/public'; -import { registerDataHandler } from './data_handler'; -import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav'; import type { LensPublicStart } from '../../lens/public'; -import { createCallObservabilityApi } from './services/call_observability_api'; -import { observabilityRuleRegistrySettings } from '../common/observability_rule_registry'; +import type { RuleRegistryPublicPluginSetupContract } from '../../rule_registry/public'; +import type { ObservabilityRuleFieldMap } from '../common/rules/observability_rule_field_map'; +import { observabilityRuleRegistrySettings } from '../common/rules/observability_rule_registry_settings'; +import { registerDataHandler } from './data_handler'; import { FormatterRuleRegistry } from './rules/formatter_rule_registry'; +import { createCallObservabilityApi } from './services/call_observability_api'; +import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav'; export type ObservabilityPublicSetup = ReturnType; export type ObservabilityRuleRegistry = ObservabilityPublicSetup['ruleRegistry']; @@ -72,6 +73,7 @@ export class Plugin const observabilityRuleRegistry = pluginsSetup.ruleRegistry.registry.create({ ...observabilityRuleRegistrySettings, + fieldMap: {} as ObservabilityRuleFieldMap, ctor: FormatterRuleRegistry, }); diff --git a/x-pack/plugins/observability/public/rules/formatter_rule_registry.ts b/x-pack/plugins/observability/public/rules/formatter_rule_registry.ts index 87e6b3c324634..0d0d22cf750fb 100644 --- a/x-pack/plugins/observability/public/rules/formatter_rule_registry.ts +++ b/x-pack/plugins/observability/public/rules/formatter_rule_registry.ts @@ -7,12 +7,17 @@ import type { RuleType } from '../../../rule_registry/public'; import type { BaseRuleFieldMap, OutputOfFieldMap } from '../../../rule_registry/common'; import { RuleRegistry } from '../../../rule_registry/public'; +import type { asDuration, asPercent } from '../../common/utils/formatters'; type AlertTypeOf = OutputOfFieldMap; type FormattableRuleType = RuleType & { format?: (options: { alert: AlertTypeOf; + formatters: { + asDuration: typeof asDuration; + asPercent: typeof asPercent; + }; }) => { reason?: string; link?: string; diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index b167600e788a4..b5208260297d0 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -16,7 +16,8 @@ import type { RuleRegistryPluginSetupContract } from '../../rule_registry/server import { uiSettings } from './ui_settings'; import { registerRoutes } from './routes/register_routes'; import { getGlobalObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository'; -import { observabilityRuleRegistrySettings } from '../common/observability_rule_registry'; +import { observabilityRuleRegistrySettings } from '../common/rules/observability_rule_registry_settings'; +import { observabilityRuleFieldMap } from '../common/rules/observability_rule_field_map'; export type ObservabilityPluginSetup = ReturnType; export type ObservabilityRuleRegistry = ObservabilityPluginSetup['ruleRegistry']; @@ -50,9 +51,10 @@ export class ObservabilityPlugin implements Plugin { }); } - const observabilityRuleRegistry = plugins.ruleRegistry.create( - observabilityRuleRegistrySettings - ); + const observabilityRuleRegistry = plugins.ruleRegistry.create({ + ...observabilityRuleRegistrySettings, + fieldMap: observabilityRuleFieldMap, + }); registerRoutes({ core: { diff --git a/x-pack/plugins/osquery/public/agents/agent_grouper.ts b/x-pack/plugins/osquery/public/agents/agent_grouper.ts new file mode 100644 index 0000000000000..419a3b9e733a4 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/agent_grouper.ts @@ -0,0 +1,118 @@ +/* + * 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 { Agent } from '../../common/shared_imports'; +import { generateColorPicker } from './helpers'; +import { + ALL_AGENTS_LABEL, + AGENT_PLATFORMS_LABEL, + AGENT_POLICY_LABEL, + AGENT_SELECTION_LABEL, +} from './translations'; +import { AGENT_GROUP_KEY, Group, GroupOption } from './types'; + +const getColor = generateColorPicker(); + +const generateGroup = (label: string, groupType: AGENT_GROUP_KEY) => { + return { + label, + groupType, + color: getColor(groupType), + size: 0, + data: [] as T[], + }; +}; + +export class AgentGrouper { + groupOrder = [ + AGENT_GROUP_KEY.All, + AGENT_GROUP_KEY.Platform, + AGENT_GROUP_KEY.Policy, + AGENT_GROUP_KEY.Agent, + ]; + groups = { + [AGENT_GROUP_KEY.All]: generateGroup(ALL_AGENTS_LABEL, AGENT_GROUP_KEY.All), + [AGENT_GROUP_KEY.Platform]: generateGroup(AGENT_PLATFORMS_LABEL, AGENT_GROUP_KEY.Platform), + [AGENT_GROUP_KEY.Policy]: generateGroup(AGENT_POLICY_LABEL, AGENT_GROUP_KEY.Policy), + [AGENT_GROUP_KEY.Agent]: generateGroup(AGENT_SELECTION_LABEL, AGENT_GROUP_KEY.Agent), + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateGroup(key: AGENT_GROUP_KEY, data: any[], append = false) { + if (!data?.length) { + return; + } + const group = this.groups[key]; + if (append) { + group.data.push(...data); + } else { + group.data = data; + } + group.size = data.length; + } + + setTotalAgents(total: number): void { + this.groups[AGENT_GROUP_KEY.All].size = total; + } + + generateOptions(): GroupOption[] { + const opts: GroupOption[] = []; + for (const key of this.groupOrder) { + const { label, size, groupType, data, color } = this.groups[key]; + if (size === 0) { + continue; + } + + switch (key) { + case AGENT_GROUP_KEY.All: + opts.push({ + label, + options: [ + { + label, + value: { groupType, size }, + color, + }, + ], + }); + break; + case AGENT_GROUP_KEY.Platform: + case AGENT_GROUP_KEY.Policy: + opts.push({ + label, + options: (data as Group[]).map(({ name, id, size: groupSize }) => ({ + label: name !== id ? `${name} (${id})` : name, + key: id, + color: getColor(groupType), + value: { groupType, id, size: groupSize }, + })), + }); + break; + case AGENT_GROUP_KEY.Agent: + opts.push({ + label, + options: (data as Agent[]).map((agent: Agent) => ({ + label: `${agent.local_metadata.host.hostname} (${agent.local_metadata.elastic.agent.id})`, + key: agent.local_metadata.elastic.agent.id, + color, + value: { + groupType, + groups: { + policy: agent.policy_id ?? '', + platform: agent.local_metadata.os.platform, + }, + id: agent.local_metadata.elastic.agent.id, + online: agent.active, + }, + })), + }); + break; + } + } + return opts; + } +} diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index 5f1b6a0d2f0b1..38132957c341f 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -5,179 +5,98 @@ * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; -import { EuiComboBox, EuiComboBoxOptionOption, EuiHealth, EuiHighlight } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { EuiComboBox, EuiHealth, EuiHighlight } from '@elastic/eui'; +import { useDebounce } from 'react-use'; import { useAllAgents } from './use_all_agents'; import { useAgentGroups } from './use_agent_groups'; import { useOsqueryPolicies } from './use_osquery_policies'; -import { Agent } from '../../common/shared_imports'; +import { AgentGrouper } from './agent_grouper'; import { getNumAgentsInGrouping, generateAgentCheck, getNumOverlapped, - generateColorPicker, + generateAgentSelection, } from './helpers'; -import { - ALL_AGENTS_LABEL, - AGENT_PLATFORMS_LABEL, - AGENT_POLICY_LABEL, - SELECT_AGENT_LABEL, - AGENT_SELECTION_LABEL, - generateSelectedAgentsMessage, -} from './translations'; - -import { AGENT_GROUP_KEY, SelectedGroups, AgentOptionValue, GroupOptionValue } from './types'; +import { SELECT_AGENT_LABEL, generateSelectedAgentsMessage } from './translations'; -export interface AgentsSelection { - agents: string[]; - allAgentsSelected: boolean; - platformsSelected: string[]; - policiesSelected: string[]; -} +import { + AGENT_GROUP_KEY, + SelectedGroups, + AgentOptionValue, + GroupOption, + AgentSelection, +} from './types'; interface AgentsTableProps { - agentSelection: AgentsSelection; - onChange: (payload: AgentsSelection) => void; + agentSelection: AgentSelection; + onChange: (payload: AgentSelection) => void; } -type GroupOption = EuiComboBoxOptionOption; - -const getColor = generateColorPicker(); +const perPage = 10; +const DEBOUNCE_DELAY = 100; // ms const AgentsTableComponent: React.FC = ({ onChange }) => { + // search related + const [searchValue, setSearchValue] = useState(''); + const [modifyingSearch, setModifyingSearch] = useState(false); + const [debouncedSearchValue, setDebouncedSearchValue] = useState(''); + useDebounce( + () => { + // update the real search value, set the typing flag + setDebouncedSearchValue(searchValue); + setModifyingSearch(false); + }, + DEBOUNCE_DELAY, + [searchValue] + ); + + // grouping related const osqueryPolicyData = useOsqueryPolicies(); const { loading: groupsLoading, totalCount: totalNumAgents, groups } = useAgentGroups( osqueryPolicyData ); - const { agents } = useAllAgents(osqueryPolicyData); - const [loading, setLoading] = useState(true); + const grouper = useMemo(() => new AgentGrouper(), []); + const { agentsLoading, agents } = useAllAgents(osqueryPolicyData, debouncedSearchValue, { + perPage, + }); + + // option related const [options, setOptions] = useState([]); const [selectedOptions, setSelectedOptions] = useState([]); const [numAgentsSelected, setNumAgentsSelected] = useState(0); useEffect(() => { - const allAgentsLabel = ALL_AGENTS_LABEL; - const opts: GroupOption[] = [ - { - label: allAgentsLabel, - options: [ - { - label: allAgentsLabel, - value: { groupType: AGENT_GROUP_KEY.All, size: totalNumAgents }, - color: getColor(AGENT_GROUP_KEY.All), - }, - ], - }, - ]; - - if (groups.platforms.length > 0) { - const groupType = AGENT_GROUP_KEY.Platform; - opts.push({ - label: AGENT_PLATFORMS_LABEL, - options: groups.platforms.map(({ name, size }) => ({ - label: name, - color: getColor(groupType), - value: { groupType, size }, - })), - }); - } - - if (groups.policies.length > 0) { - const groupType = AGENT_GROUP_KEY.Policy; - opts.push({ - label: AGENT_POLICY_LABEL, - options: groups.policies.map(({ name, size }) => ({ - label: name, - color: getColor(groupType), - value: { groupType, size }, - })), - }); - } - - if (agents && agents.length > 0) { - const groupType = AGENT_GROUP_KEY.Agent; - opts.push({ - label: AGENT_SELECTION_LABEL, - options: (agents as Agent[]).map((agent: Agent) => ({ - label: agent.local_metadata.host.hostname, - color: getColor(groupType), - value: { - groupType, - groups: { policy: agent.policy_id ?? '', platform: agent.local_metadata.os.platform }, - id: agent.local_metadata.elastic.agent.id, - online: agent.active, - }, - })), - }); - } - setLoading(false); - setOptions(opts); - }, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents]); + // update the groups when groups or agents have changed + grouper.setTotalAgents(totalNumAgents); + grouper.updateGroup(AGENT_GROUP_KEY.Platform, groups.platforms); + grouper.updateGroup(AGENT_GROUP_KEY.Policy, groups.policies); + grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents); + const newOptions = grouper.generateOptions(); + setOptions(newOptions); + }, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents, grouper]); const onSelection = useCallback( (selection: GroupOption[]) => { - // TODO?: optimize this by making it incremental - const newAgentSelection: AgentsSelection = { - agents: [], - allAgentsSelected: false, - platformsSelected: [], - policiesSelected: [], - }; - // parse through the selections to be able to determine how many are actually selected - const selectedAgents = []; - const selectedGroups: SelectedGroups = { - policy: {}, - platform: {}, - }; - - // TODO: clean this up, make it less awkward - for (const opt of selection) { - const groupType = opt.value?.groupType; - let value; - switch (groupType) { - case AGENT_GROUP_KEY.All: - newAgentSelection.allAgentsSelected = true; - break; - case AGENT_GROUP_KEY.Platform: - value = opt.value as GroupOptionValue; - if (!newAgentSelection.allAgentsSelected) { - // we don't need to calculate diffs when all agents are selected - selectedGroups.platform[opt.label] = value.size; - } - newAgentSelection.platformsSelected.push(opt.label); - break; - case AGENT_GROUP_KEY.Policy: - value = opt.value as GroupOptionValue; - if (!newAgentSelection.allAgentsSelected) { - // we don't need to calculate diffs when all agents are selected - selectedGroups.policy[opt.label] = value.size ?? 0; - } - newAgentSelection.policiesSelected.push(opt.label); - break; - case AGENT_GROUP_KEY.Agent: - value = opt.value as AgentOptionValue; - if (!newAgentSelection.allAgentsSelected) { - // we don't need to count how many agents are selected if they are all selected - selectedAgents.push(opt.value); - } - // TODO: fix this casting by updating the opt type to be a union - newAgentSelection.agents.push(value.id as string); - break; - default: - // this should never happen! - // eslint-disable-next-line no-console - console.error(`unknown group type ${groupType}`); - } - } + // TODO?: optimize this by making the selection computation incremental + const { + newAgentSelection, + selectedAgents, + selectedGroups, + }: { + newAgentSelection: AgentSelection; + selectedAgents: AgentOptionValue[]; + selectedGroups: SelectedGroups; + } = generateAgentSelection(selection); if (newAgentSelection.allAgentsSelected) { setNumAgentsSelected(totalNumAgents); } else { const checkAgent = generateAgentCheck(selectedGroups); setNumAgentsSelected( // filter out all the agents counted by selected policies and platforms - selectedAgents.filter((a) => checkAgent(a as AgentOptionValue)).length + + selectedAgents.filter(checkAgent).length + // add the number of agents added via policy and platform groups getNumAgentsInGrouping(selectedGroups) - // subtract the number of agents double counted by policy/platform selections @@ -190,32 +109,40 @@ const AgentsTableComponent: React.FC = ({ onChange }) => { [groups, onChange, totalNumAgents] ); - const renderOption = useCallback((option, searchValue, contentClassName) => { + const renderOption = useCallback((option, searchVal, contentClassName) => { const { label, value } = option; return value?.groupType === AGENT_GROUP_KEY.Agent ? ( - {label} + {label} ) : ( - {label} + [{value?.size ?? 0}]   - ({value?.size}) + {label} ); }, []); + + const onSearchChange = useCallback((v: string) => { + // set the typing flag and update the search value + setModifyingSearch(v !== ''); + setSearchValue(v); + }, []); + return (
-

{SELECT_AGENT_LABEL}

{numAgentsSelected > 0 ? {generateSelectedAgentsMessage(numAgentsSelected)} : ''}   { const { platforms, policies, overlap } = processAggregations(input); expect(platforms).toEqual([ { + id: 'darwin', name: 'darwin', size: 200, }, @@ -59,10 +60,12 @@ describe('processAggregations', () => { expect(platforms).toEqual([]); expect(policies).toEqual([ { + id: '8cd01a60-8a74-11eb-86cb-c58693443a4f', name: '8cd01a60-8a74-11eb-86cb-c58693443a4f', size: 100, }, { + id: '8cd06880-8a74-11eb-86cb-c58693443a4f', name: '8cd06880-8a74-11eb-86cb-c58693443a4f', size: 100, }, @@ -107,16 +110,19 @@ describe('processAggregations', () => { const { platforms, policies, overlap } = processAggregations(input); expect(platforms).toEqual([ { + id: 'darwin', name: 'darwin', size: 200, }, ]); expect(policies).toEqual([ { + id: '8cd01a60-8a74-11eb-86cb-c58693443a4f', name: '8cd01a60-8a74-11eb-86cb-c58693443a4f', size: 100, }, { + id: '8cd06880-8a74-11eb-86cb-c58693443a4f', name: '8cd06880-8a74-11eb-86cb-c58693443a4f', size: 100, }, diff --git a/x-pack/plugins/osquery/public/agents/helpers.ts b/x-pack/plugins/osquery/public/agents/helpers.ts index 830fca5f57caa..14a8dd64fb4da 100644 --- a/x-pack/plugins/osquery/public/agents/helpers.ts +++ b/x-pack/plugins/osquery/public/agents/helpers.ts @@ -20,6 +20,9 @@ import { Group, AgentOptionValue, AggregationDataPoint, + AgentSelection, + GroupOptionValue, + GroupOption, } from './types'; export type InspectResponse = Inspect & { response: string[] }; @@ -43,11 +46,12 @@ export const processAggregations = (aggs: Record) => { const platformTerms = aggs.platforms as TermsAggregate; const policyTerms = aggs.policies as TermsAggregate; - const policies = policyTerms?.buckets.map((o) => ({ name: o.key, size: o.doc_count })) ?? []; + const policies = + policyTerms?.buckets.map((o) => ({ name: o.key, id: o.key, size: o.doc_count })) ?? []; if (platformTerms?.buckets) { for (const { key, doc_count: size, policies: platformPolicies } of platformTerms.buckets) { - platforms.push({ name: key, size }); + platforms.push({ name: key, id: key, size }); if (platformPolicies?.buckets && policies.length > 0) { overlap[key] = platformPolicies.buckets.reduce((acc: { [key: string]: number }, pol) => { acc[pol.key] = pol.doc_count; @@ -96,6 +100,63 @@ export const generateAgentCheck = (selectedGroups: SelectedGroups) => { }; }; +export const generateAgentSelection = (selection: GroupOption[]) => { + const newAgentSelection: AgentSelection = { + agents: [], + allAgentsSelected: false, + platformsSelected: [], + policiesSelected: [], + }; + // parse through the selections to be able to determine how many are actually selected + const selectedAgents: AgentOptionValue[] = []; + const selectedGroups: SelectedGroups = { + policy: {}, + platform: {}, + }; + + // TODO: clean this up, make it less awkward + for (const opt of selection) { + const groupType = opt.value?.groupType; + let value; + switch (groupType) { + case AGENT_GROUP_KEY.All: + newAgentSelection.allAgentsSelected = true; + break; + case AGENT_GROUP_KEY.Platform: + value = opt.value as GroupOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to calculate diffs when all agents are selected + selectedGroups.platform[opt.value?.id ?? opt.label] = value.size; + } + newAgentSelection.platformsSelected.push(opt.label); + break; + case AGENT_GROUP_KEY.Policy: + value = opt.value as GroupOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to calculate diffs when all agents are selected + selectedGroups.policy[opt.value?.id ?? opt.label] = value.size; + } + newAgentSelection.policiesSelected.push(opt.label); + break; + case AGENT_GROUP_KEY.Agent: + value = opt.value as AgentOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to count how many agents are selected if they are all selected + selectedAgents.push(value); + } + if (value?.id) { + newAgentSelection.agents.push(value.id); + } + break; + default: + // this should never happen! + // eslint-disable-next-line no-console + console.error(`unknown group type ${groupType}`); + } + } + return { newAgentSelection, selectedGroups, selectedAgents }; +}; + export const generateTablePaginationOptions = ( activePage: number, limit: number, diff --git a/x-pack/plugins/osquery/public/agents/translations.ts b/x-pack/plugins/osquery/public/agents/translations.ts index af99a73d63de2..209761b4c8bdf 100644 --- a/x-pack/plugins/osquery/public/agents/translations.ts +++ b/x-pack/plugins/osquery/public/agents/translations.ts @@ -40,7 +40,7 @@ export const AGENT_SELECTION_LABEL = i18n.translate('xpack.osquery.agents.select }); export const SELECT_AGENT_LABEL = i18n.translate('xpack.osquery.agents.selectAgentLabel', { - defaultMessage: `Select Agents`, + defaultMessage: `Select agents or groups`, }); export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { diff --git a/x-pack/plugins/osquery/public/agents/types.ts b/x-pack/plugins/osquery/public/agents/types.ts index 2fa8ddaf345cd..b26404f9c5e70 100644 --- a/x-pack/plugins/osquery/public/agents/types.ts +++ b/x-pack/plugins/osquery/public/agents/types.ts @@ -6,6 +6,7 @@ */ import { TermsAggregate } from '@elastic/elasticsearch/api/types'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; interface BaseDataPoint { key: string; @@ -17,6 +18,7 @@ export type AggregationDataPoint = BaseDataPoint & { }; export interface Group { + id: string; name: string; size: number; } @@ -28,14 +30,23 @@ export interface SelectedGroups { [groupType: string]: { [groupName: string]: number }; } +export type GroupOption = EuiComboBoxOptionOption; + +export interface AgentSelection { + agents: string[]; + allAgentsSelected: boolean; + platformsSelected: string[]; + policiesSelected: string[]; +} + interface BaseGroupOption { + id?: string; groupType: AGENT_GROUP_KEY; } export type AgentOptionValue = BaseGroupOption & { groups: { [groupType: string]: string }; online: boolean; - id: string; }; export type GroupOptionValue = BaseGroupOption & { diff --git a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts index 0eaca65d02d4b..0853891f1919d 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts @@ -7,6 +7,7 @@ import { useState } from 'react'; import { useQuery } from 'react-query'; import { useKibana } from '../common/lib/kibana'; +import { useAgentPolicies } from './use_agent_policies'; import { OsqueryQueries, @@ -25,6 +26,7 @@ interface UseAgentGroups { export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAgentGroups) => { const { data } = useKibana().services; + const { agentPoliciesLoading, agentPolicyById } = useAgentPolicies(osqueryPolicies); const [platforms, setPlatforms] = useState([]); const [policies, setPolicies] = useState([]); const [loading, setLoading] = useState(true); @@ -78,14 +80,22 @@ export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseA setPlatforms(newPlatforms); setOverlap(newOverlap); - setPolicies(newPolicies); + setPolicies( + newPolicies.map((p) => { + const name = agentPolicyById[p.id]?.name ?? p.name; + return { + ...p, + name, + }; + }) + ); } setLoading(false); setTotalCount(responseData.totalCount); }, { - enabled: !osqueryPoliciesLoading, + enabled: !osqueryPoliciesLoading && !agentPoliciesLoading, } ); diff --git a/x-pack/plugins/osquery/public/agents/use_agent_policies.ts b/x-pack/plugins/osquery/public/agents/use_agent_policies.ts new file mode 100644 index 0000000000000..3045423ccbe2d --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/use_agent_policies.ts @@ -0,0 +1,38 @@ +/* + * 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 { useQueries, UseQueryResult } from 'react-query'; +import { useKibana } from '../common/lib/kibana'; +import { + AgentPolicy, + agentPolicyRouteService, + GetOneAgentPolicyResponse, +} from '../../../fleet/common'; + +export const useAgentPolicies = (policyIds: string[] = []) => { + const { http } = useKibana().services; + + const agentResponse = useQueries( + policyIds.map((policyId) => ({ + queryKey: ['agentPolicy', policyId], + queryFn: () => http.get(agentPolicyRouteService.getInfoPath(policyId)), + enabled: policyIds.length > 0, + })) + ) as Array>; + + const agentPoliciesLoading = agentResponse.some((p) => p.isLoading); + const agentPolicies = agentResponse.map((p) => p.data?.item); + const agentPolicyById = agentPolicies.reduce((acc, p) => { + if (!p) { + return acc; + } + acc[p.id] = p; + return acc; + }, {} as { [key: string]: AgentPolicy }); + + return { agentPoliciesLoading, agentPolicies, agentPolicyById }; +}; diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts index 607f9ae007692..bd9b1c32412e6 100644 --- a/x-pack/plugins/osquery/public/agents/use_all_agents.ts +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -14,16 +14,30 @@ interface UseAllAgents { osqueryPoliciesLoading: boolean; } -export const useAllAgents = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAllAgents) => { - // TODO: properly fetch these in an async manner +interface RequestOptions { + perPage?: number; + page?: number; +} + +// TODO: break out the paginated vs all cases into separate hooks +export const useAllAgents = ( + { osqueryPolicies, osqueryPoliciesLoading }: UseAllAgents, + searchValue = '', + opts: RequestOptions = { perPage: 9000 } +) => { + const { perPage } = opts; const { http } = useKibana().services; const { isLoading: agentsLoading, data: agentData } = useQuery( - ['agents', osqueryPolicies], + ['agents', osqueryPolicies, searchValue, perPage], async () => { + let kuery = `(${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')})`; + if (searchValue) { + kuery += ` and (local_metadata.host.hostname:/${searchValue}/ or local_metadata.elastic.agent.id:/${searchValue}/)`; + } return await http.get('/api/fleet/agents', { query: { - kuery: osqueryPolicies.map((p) => `policy_id:${p}`).join(' or '), - perPage: 9000, + kuery, + perPage, }, }); }, diff --git a/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx index 4bc9262af7613..ccde0fd8305f9 100644 --- a/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx +++ b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx @@ -7,10 +7,11 @@ import React, { useCallback } from 'react'; import { FieldHook } from '../../shared_imports'; -import { AgentsTable, AgentsSelection } from '../../agents/agents_table'; +import { AgentsTable } from '../../agents/agents_table'; +import { AgentSelection } from '../../agents/types'; interface AgentsTableFieldProps { - field: FieldHook; + field: FieldHook; } const AgentsTableFieldComponent: React.FC = ({ field }) => { diff --git a/x-pack/plugins/rule_registry/kibana.json b/x-pack/plugins/rule_registry/kibana.json index 1636f88a21a61..ec2b366f739e6 100644 --- a/x-pack/plugins/rule_registry/kibana.json +++ b/x-pack/plugins/rule_registry/kibana.json @@ -11,8 +11,5 @@ "triggersActionsUi" ], "server": true, - "ui": true, - "extraPublicDirs": [ - "common" - ] + "ui": true } diff --git a/x-pack/plugins/rule_registry/public/index.ts b/x-pack/plugins/rule_registry/public/index.ts index 55662dbcc8bfc..59697261ff20b 100644 --- a/x-pack/plugins/rule_registry/public/index.ts +++ b/x-pack/plugins/rule_registry/public/index.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { PluginInitializerContext } from 'kibana/public'; +import type { PluginInitializerContext } from 'kibana/public'; import { Plugin } from './plugin'; -export { RuleRegistryPublicPluginSetupContract } from './plugin'; +export type { RuleRegistryPublicPluginSetupContract } from './plugin'; export { RuleRegistry } from './rule_registry'; export type { IRuleRegistry, RuleType } from './rule_registry/types'; diff --git a/x-pack/plugins/rule_registry/public/plugin.ts b/x-pack/plugins/rule_registry/public/plugin.ts index 66c9a4fa224a5..7f0bceefb6797 100644 --- a/x-pack/plugins/rule_registry/public/plugin.ts +++ b/x-pack/plugins/rule_registry/public/plugin.ts @@ -19,7 +19,7 @@ import type { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; -import { baseRuleFieldMap } from '../common'; +import type { BaseRuleFieldMap } from '../common'; import { RuleRegistry } from './rule_registry'; interface RuleRegistrySetupPlugins { @@ -40,7 +40,7 @@ export class Plugin public setup(core: CoreSetup, plugins: RuleRegistrySetupPlugins) { const rootRegistry = new RuleRegistry({ - fieldMap: baseRuleFieldMap, + fieldMap: {} as BaseRuleFieldMap, alertTypeRegistry: plugins.triggersActionsUi.alertTypeRegistry, }); return { diff --git a/x-pack/plugins/rule_registry/public/rule_registry/types.ts b/x-pack/plugins/rule_registry/public/rule_registry/types.ts index bb16227cbab5f..7c186385ebd35 100644 --- a/x-pack/plugins/rule_registry/public/rule_registry/types.ts +++ b/x-pack/plugins/rule_registry/public/rule_registry/types.ts @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { AlertTypeRegistryContract } from '../../../triggers_actions_ui/public'; -import { BaseRuleFieldMap, FieldMap } from '../../common'; +import type { AlertTypeRegistryContract } from '../../../triggers_actions_ui/public'; +import type { BaseRuleFieldMap, FieldMap } from '../../common'; export interface RuleRegistryConstructorOptions { fieldMap: TFieldMap; diff --git a/x-pack/plugins/saved_objects_tagging/public/components/base/tag_badge.tsx b/x-pack/plugins/saved_objects_tagging/public/components/base/tag_badge.tsx index 6bc9e659d9346..e8af661d6921d 100644 --- a/x-pack/plugins/saved_objects_tagging/public/components/base/tag_badge.tsx +++ b/x-pack/plugins/saved_objects_tagging/public/components/base/tag_badge.tsx @@ -17,5 +17,9 @@ export interface TagBadgeProps { * The badge representation of a Tag, which is the default display to be used for them. */ export const TagBadge: FC = ({ tag }) => { - return {tag.name}; + return ( + + {tag.name} + + ); }; diff --git a/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts b/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts index 08b5a0f60521c..2034a4e5b74ba 100644 --- a/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts +++ b/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts @@ -9,11 +9,7 @@ import { BehaviorSubject } from 'rxjs'; import { UnwrapPromise } from '@kbn/utility-types'; import supertest from 'supertest'; -import { - LegacyAPICaller, - ServiceStatus, - ServiceStatusLevels, -} from '../../../../../src/core/server'; +import { ServiceStatus, ServiceStatusLevels } from '../../../../../src/core/server'; import { contextServiceMock, elasticsearchServiceMock, @@ -31,24 +27,18 @@ export function mockGetClusterInfo(clusterInfo: any) { esClient.info.mockResolvedValue({ body: { ...clusterInfo } }); return esClient; } + describe('/api/settings', () => { let server: HttpService; let httpSetup: HttpSetup; let overallStatus$: BehaviorSubject; - let mockApiCaller: jest.Mocked; beforeEach(async () => { - mockApiCaller = jest.fn(); server = createHttpServer(); httpSetup = await server.setup({ context: contextServiceMock.createSetupContract({ core: { elasticsearch: { - legacy: { - client: { - callAsCurrentUser: mockApiCaller, - }, - }, client: { asCurrentUser: mockGetClusterInfo({ cluster_uuid: 'yyy-yyyyy' }), }, diff --git a/x-pack/plugins/xpack_legacy/server/routes/settings.ts b/x-pack/plugins/xpack_legacy/server/routes/settings.ts index 9117637b70bee..b9052ca0c84e3 100644 --- a/x-pack/plugins/xpack_legacy/server/routes/settings.ts +++ b/x-pack/plugins/xpack_legacy/server/routes/settings.ts @@ -42,9 +42,7 @@ export function registerSettingsRoute({ validate: false, }, async (context, req, res) => { - const { callAsCurrentUser } = context.core.elasticsearch.legacy.client; const collectorFetchContext = { - callCluster: callAsCurrentUser, esClient: context.core.elasticsearch.client.asCurrentUser, soClient: context.core.savedObjects.client, }; diff --git a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts index deb91f6b9b1ef..51875c683346e 100644 --- a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts +++ b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts @@ -96,8 +96,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('can open job selection flyout', async () => { await PageObjects.dashboard.clickCreateDashboardPrompt(); await ml.dashboardEmbeddables.assertDashboardIsEmpty(); - await dashboardAddPanel.clickOpenAddPanel(); - await dashboardAddPanel.ensureAddPanelIsShowing(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickEmbeddableFactoryGroupButton('ml'); await dashboardAddPanel.clickAddNewEmbeddableLink('ml_anomaly_charts'); await ml.dashboardJobSelectionTable.assertJobSelectionTableExists(); await a11y.testAppSnapshot(); diff --git a/x-pack/test/api_integration/apis/lens/existing_fields.ts b/x-pack/test/api_integration/apis/lens/existing_fields.ts index 88949401f102a..0358786993919 100644 --- a/x-pack/test/api_integration/apis/lens/existing_fields.ts +++ b/x-pack/test/api_integration/apis/lens/existing_fields.ts @@ -160,7 +160,8 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('existing_fields apis', () => { + // FLAKY: https://github.com/elastic/kibana/issues/97387 + describe.skip('existing_fields apis', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('visualize/default'); diff --git a/x-pack/test/examples/search_examples/index.ts b/x-pack/test/examples/search_examples/index.ts index 13eac7566525e..65e214cda4cf8 100644 --- a/x-pack/test/examples/search_examples/index.ts +++ b/x-pack/test/examples/search_examples/index.ts @@ -23,7 +23,8 @@ export default function ({ getService, loadTestFile }: PluginFunctionalProviderC await esArchiver.unload('lens/basic'); }); - loadTestFile(require.resolve('./search_sessions_cache')); loadTestFile(require.resolve('./search_session_example')); + loadTestFile(require.resolve('./search_example')); + loadTestFile(require.resolve('./search_sessions_cache')); }); } diff --git a/x-pack/test/examples/search_examples/search_example.ts b/x-pack/test/examples/search_examples/search_example.ts new file mode 100644 index 0000000000000..19a9535ebb951 --- /dev/null +++ b/x-pack/test/examples/search_examples/search_example.ts @@ -0,0 +1,38 @@ +/* + * 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 { FtrProviderContext } from '../../functional/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'timePicker']); + const retry = getService('retry'); + + describe.skip('Search session example', () => { + const appId = 'searchExamples'; + + before(async function () { + await PageObjects.common.navigateToApp(appId, { insertTimestamp: false }); + }); + + it('should have an other bucket', async () => { + await PageObjects.timePicker.setAbsoluteRange( + 'Jan 1, 2014 @ 00:00:00.000', + 'Jan 1, 2016 @ 00:00:00.000' + ); + await testSubjects.click('searchSourceWithOther'); + + await retry.waitFor('has other bucket', async () => { + await testSubjects.click('responseTab'); + const codeBlock = await testSubjects.find('responseCodeBlock'); + const visibleText = await codeBlock.getVisibleText(); + return visibleText.indexOf('__other__') > -1; + }); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js index 009e1a2dad5f1..445d9706bb9a9 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/index.js +++ b/x-pack/test/fleet_api_integration/apis/epm/index.js @@ -24,5 +24,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./update_assets')); loadTestFile(require.resolve('./data_stream')); loadTestFile(require.resolve('./package_install_complete')); + loadTestFile(require.resolve('./install_error_rollback')); }); } diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_error_rollback.ts b/x-pack/test/fleet_api_integration/apis/epm/install_error_rollback.ts new file mode 100644 index 0000000000000..6e2ea3b96aa58 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/epm/install_error_rollback.ts @@ -0,0 +1,61 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const goodPackage = 'error_handling-0.1.0'; + const badPackage = 'error_handling-0.2.0'; + + const installPackage = async (pkgkey: string) => { + await supertest + .post(`/api/fleet/epm/packages/${pkgkey}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + }; + + const getPackageInfo = async (pkgkey: string) => { + return await supertest.get(`/api/fleet/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); + }; + + describe('package installation error handling and rollback', async () => { + skipIfNoDockerRegistry(providerContext); + beforeEach(async () => { + await esArchiver.load('empty_kibana'); + }); + afterEach(async () => { + await esArchiver.unload('empty_kibana'); + }); + + it('on a fresh install, it should uninstall a broken package during rollback', async function () { + await supertest + .post(`/api/fleet/epm/packages/${badPackage}`) + .set('kbn-xsrf', 'xxxx') + .expect(422); // the broken package contains a broken visualization triggering a 422 from Kibana + + const pkgInfoResponse = await getPackageInfo(badPackage); + expect(JSON.parse(pkgInfoResponse.text).response.status).to.be('not_installed'); + }); + + it('on an upgrade, it should fall back to the previous good version during rollback', async function () { + await installPackage(goodPackage); + await supertest + .post(`/api/fleet/epm/packages/${badPackage}`) + .set('kbn-xsrf', 'xxxx') + .expect(422); // the broken package contains a broken visualization triggering a 422 from Kibana + + const goodPkgInfoResponse = await getPackageInfo(goodPackage); + expect(JSON.parse(goodPkgInfoResponse.text).response.status).to.be('installed'); + expect(JSON.parse(goodPkgInfoResponse.text).response.version).to.be('0.1.0'); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/docs/README.md new file mode 100644 index 0000000000000..260499f4b0078 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +This package should install without errors. + +Version 0.2.0 of this package should fail during installation. We need this good version to test rollback. \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/kibana/visualization/sample_visualization.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/kibana/visualization/sample_visualization.json new file mode 100644 index 0000000000000..01afe600853ef --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/kibana/visualization/sample_visualization.json @@ -0,0 +1,14 @@ +{ + "attributes": { + "description": "sample visualization", + "title": "sample vis title", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customLabel\":\"Log Level\",\"field\":\"log.level\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"group\",\"type\":\"terms\"}],\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"@timestamp per day\"},\"type\":\"category\"}],\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"histogram\",\"valueAxis\":\"ValueAxis-1\"}],\"times\":[],\"type\":\"histogram\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Count\"},\"type\":\"value\"}]},\"title\":\"Log levels over time [Logs Kafka] ECS\",\"type\":\"histogram\"}" + }, + "id": "sample_visualization", + "type": "visualization", + "migrationVersion": { + "visualization": "7.7.0" + } +} diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/manifest.yml new file mode 100644 index 0000000000000..bba1a6a4c347d --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: error_handling +title: Error handling +description: tests error handling and rollback +version: 0.1.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/docs/README.md new file mode 100644 index 0000000000000..c348f801b1780 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/docs/README.md @@ -0,0 +1,5 @@ +This package should fail during installation. + +Version 0.1.0 of this package should install without errors, and be rolled back to without errors. + +This package contains one Kibana visualization that requires a non-existent version of Kibana in order to trigger an error during installation. \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/kibana/visualization/sample_visualization.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/kibana/visualization/sample_visualization.json new file mode 100644 index 0000000000000..0a4867cfe1c11 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/kibana/visualization/sample_visualization.json @@ -0,0 +1,14 @@ +{ + "attributes": { + "description": "sample visualization", + "title": "sample vis title", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customLabel\":\"Log Level\",\"field\":\"log.level\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"group\",\"type\":\"terms\"}],\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"@timestamp per day\"},\"type\":\"category\"}],\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"histogram\",\"valueAxis\":\"ValueAxis-1\"}],\"times\":[],\"type\":\"histogram\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Count\"},\"type\":\"value\"}]},\"title\":\"Log levels over time [Logs Kafka] ECS\",\"type\":\"histogram\"}" + }, + "id": "sample_visualization", + "type": "visualization", + "migrationVersion": { + "visualization": "12.7.0" + } +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml new file mode 100644 index 0000000000000..2eb6a41a77ede --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml @@ -0,0 +1,19 @@ +format_version: 1.0.0 +name: error_handling +title: Error handling +description: tests error handling and rollback +version: 0.2.0 +categories: [] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts index 56a8ab46a57da..87ecfe0dcada9 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts @@ -15,7 +15,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); const dashboardPanelActions = getService('dashboardPanelActions'); - const dashboardVisualizations = getService('dashboardVisualizations'); describe('dashboard lens by value', function () { before(async () => { @@ -27,7 +26,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('can add a lens panel by value', async () => { - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); await PageObjects.lens.createAndAddLensFromDashboard({}); const newPanelCount = await PageObjects.dashboard.getPanelCount(); expect(newPanelCount).to.eql(1); diff --git a/x-pack/test/functional/apps/dashboard/dashboard_maps_by_value.ts b/x-pack/test/functional/apps/dashboard/dashboard_maps_by_value.ts index 15c76c3367a86..487dc90e1877e 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_maps_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/dashboard_maps_by_value.ts @@ -19,10 +19,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const log = getService('log'); const esArchiver = getService('esArchiver'); - const dashboardVisualizations = getService('dashboardVisualizations'); const dashboardPanelActions = getService('dashboardPanelActions'); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); + const dashboardAddPanel = getService('dashboardAddPanel'); const LAYER_NAME = 'World Countries'; let mapCounter = 0; @@ -33,7 +33,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { if (inViewMode) { await PageObjects.dashboard.switchToEditMode(); } - await PageObjects.visualize.clickMapsApp(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickVisType('maps'); await PageObjects.maps.clickSaveAndReturnButton(); } @@ -82,8 +83,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('adding a map by value', () => { it('can add a map by value', async () => { await createNewDashboard(); - - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); await createAndAddMapByValue(); const newPanelCount = await PageObjects.dashboard.getPanelCount(); expect(newPanelCount).to.eql(1); @@ -93,7 +92,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('editing a map by value', () => { before(async () => { await createNewDashboard(); - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); await createAndAddMapByValue(); await editByValueMap(); }); @@ -112,7 +110,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('editing a map and adding to map library', () => { beforeEach(async () => { await createNewDashboard(); - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); await createAndAddMapByValue(); }); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts index 3ebc53cc7cf27..730c00a8d5e4f 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts @@ -21,7 +21,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'lens', ]); - const dashboardVisualizations = getService('dashboardVisualizations'); + const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardExpect = getService('dashboardExpect'); const testSubjects = getService('testSubjects'); @@ -85,7 +85,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('can add a lens panel by value', async () => { - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); await PageObjects.lens.createAndAddLensFromDashboard({}); const newPanelCount = await PageObjects.dashboard.getPanelCount(); expect(newPanelCount).to.eql(1); @@ -171,9 +170,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); await PageObjects.dashboard.waitForRenderComplete(); - await testSubjects.click('dashboardAddNewPanelButton'); - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); - await PageObjects.visualize.clickMarkdownWidget(); + await dashboardAddPanel.clickMarkdownQuickButton(); await PageObjects.visEditor.setMarkdownTxt(originalMarkdownText); await PageObjects.visEditor.clickGo(); diff --git a/x-pack/test/functional/apps/dashboard/sync_colors.ts b/x-pack/test/functional/apps/dashboard/sync_colors.ts index 7e54f966870c3..09575c355913e 100644 --- a/x-pack/test/functional/apps/dashboard/sync_colors.ts +++ b/x-pack/test/functional/apps/dashboard/sync_colors.ts @@ -49,7 +49,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await elasticChart.setNewChartUiDebugFlag(true); await PageObjects.dashboard.clickCreateDashboardPrompt(); await dashboardAddPanel.clickCreateNewLink(); - await dashboardAddPanel.clickVisType('lens'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.lens.goToTimeRange(); @@ -68,7 +67,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.save('vis1', false, true); await PageObjects.header.waitUntilLoadingHasFinished(); await dashboardAddPanel.clickCreateNewLink(); - await dashboardAddPanel.clickVisType('lens'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.lens.configureDimension({ diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js index 57925ad50d155..37311de534195 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js @@ -10,7 +10,6 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); - const dashboardVisualizations = getService('dashboardVisualizations'); const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens']); @@ -29,9 +28,6 @@ export default function ({ getPageObjects, getService }) { it('adds Lens visualization to empty dashboard', async () => { const title = 'Dashboard Test Lens'; - await testSubjects.exists('dashboardAddNewPanelButton'); - await testSubjects.click('dashboardAddNewPanelButton'); - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); await PageObjects.lens.createAndAddLensFromDashboard({ title, redirectToOrigin: true }); await PageObjects.dashboard.waitForRenderComplete(); await testSubjects.exists(`embeddablePanelHeading-${title}`); @@ -87,9 +83,6 @@ export default function ({ getPageObjects, getService }) { const title = 'non-dashboard Test Lens'; await PageObjects.dashboard.loadSavedDashboard('empty dashboard test'); await PageObjects.dashboard.switchToEditMode(); - await testSubjects.exists('dashboardAddNewPanelButton'); - await testSubjects.click('dashboardAddNewPanelButton'); - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); await PageObjects.lens.createAndAddLensFromDashboard({ title }); await PageObjects.lens.notLinkedToOriginatingApp(); await PageObjects.common.navigateToApp('dashboard'); diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index a15176d76f953..1490abb320ca6 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -134,7 +134,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await filterBar.addFilter('geo.dest', 'is', 'LS'); await dashboardAddPanel.clickCreateNewLink(); - await dashboardAddPanel.clickVisType('lens'); await PageObjects.header.waitUntilLoadingHasFinished(); const hasGeoDestFilter = await filterBar.hasFilter('geo.dest', 'LS'); expect(hasGeoDestFilter).to.be(false); @@ -200,7 +199,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); await dashboardAddPanel.clickCreateNewLink(); - await dashboardAddPanel.clickVisType('lens'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.lens.goToTimeRange(); diff --git a/x-pack/test/functional/apps/lens/lens_tagging.ts b/x-pack/test/functional/apps/lens/lens_tagging.ts index 7ce31709498fc..6fff2baa2d0cc 100644 --- a/x-pack/test/functional/apps/lens/lens_tagging.ts +++ b/x-pack/test/functional/apps/lens/lens_tagging.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const retry = getService('retry'); const find = getService('find'); - const dashboardVisualizations = getService('dashboardVisualizations'); + const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects([ 'common', @@ -39,8 +39,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('adds a new tag to a Lens visualization', async () => { // create lens - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); - await PageObjects.visualize.clickLensWidget(); + await dashboardAddPanel.clickCreateNewLink(); await PageObjects.lens.goToTimeRange(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js index 40e73f0d8a763..9bff4e56c6c5b 100644 --- a/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js +++ b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js @@ -15,7 +15,6 @@ export default function ({ getPageObjects, getService }) { const security = getService('security'); const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); - const dashboardVisualizations = getService('dashboardVisualizations'); describe('maps in embeddable library', () => { before(async () => { @@ -34,8 +33,7 @@ export default function ({ getPageObjects, getService }) { }); await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); - await dashboardAddPanel.clickCreateNewLink(); - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await dashboardAddPanel.clickEditorMenuButton(); await PageObjects.visualize.clickMapsApp(); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.maps.waitForLayersToLoad(); diff --git a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js b/x-pack/test/functional/apps/maps/embeddable/save_and_return.js index a3abb01b4cf9f..a7e649548306b 100644 --- a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js +++ b/x-pack/test/functional/apps/maps/embeddable/save_and_return.js @@ -11,7 +11,6 @@ export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'maps', 'visualize']); const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); - const dashboardVisualizations = getService('dashboardVisualizations'); const testSubjects = getService('testSubjects'); const security = getService('security'); @@ -37,9 +36,8 @@ export default function ({ getPageObjects, getService }) { beforeEach(async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); - await dashboardAddPanel.clickCreateNewLink(); - await await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); - await PageObjects.visualize.clickMapsApp(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickVisType('maps'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.maps.waitForLayersToLoad(); }); diff --git a/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts b/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts index f7bfd7f7a4c62..0aee183c1a4a5 100644 --- a/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts +++ b/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts @@ -87,8 +87,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('can open job selection flyout', async () => { await PageObjects.dashboard.clickCreateDashboardPrompt(); await ml.dashboardEmbeddables.assertDashboardIsEmpty(); - await dashboardAddPanel.clickOpenAddPanel(); - await dashboardAddPanel.ensureAddPanelIsShowing(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickEmbeddableFactoryGroupButton('ml'); await dashboardAddPanel.clickAddNewEmbeddableLink('ml_anomaly_charts'); await ml.dashboardJobSelectionTable.assertJobSelectionTableExists(); }); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 65020be390f9d..100ed8e079d37 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -18,6 +18,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const find = getService('find'); const comboBox = getService('comboBox'); const browser = getService('browser'); + const dashboardAddPanel = getService('dashboardAddPanel'); const PageObjects = getPageObjects([ 'common', @@ -753,7 +754,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont if (inViewMode) { await PageObjects.dashboard.switchToEditMode(); } - await PageObjects.visualize.clickLensWidget(); + await dashboardAddPanel.clickCreateNewLink(); await this.goToTimeRange(); await this.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',