diff --git a/docs/visualize/timelion.asciidoc b/docs/visualize/timelion.asciidoc
index 7c7ec7c995ea7..a7520227977bc 100644
--- a/docs/visualize/timelion.asciidoc
+++ b/docs/visualize/timelion.asciidoc
@@ -43,7 +43,7 @@ image::images/timelion-create01.png[]
[[time-series-compare-data]]
==== Compare the data
-To compare the two data sets, add another series with data from the previous hour, separated by a comma:
+To compare the two data sets, add another series with data from the previous hour, separated by a comma:
[source,text]
----------------------------------
@@ -81,7 +81,7 @@ image::images/timelion-create03.png[]
[float]
[[time-series-title]]
-==== Add a title
+==== Add a title
Add a meaningful title:
@@ -169,7 +169,7 @@ Change the position and style of the legend:
[source,text]
----------------------------------
-.es(offset=-1h,index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('last hour').lines(fill=1,width=0.5).color(gray), .es(index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('current hour').title('CPU usage over time').color(#1E90FF).legend(columns=2, position=nw) <1>
+.es(offset=-1h,index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('last hour').lines(fill=1,width=0.5).color(gray), .es(index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('current hour').title('CPU usage over time').color(#1E90FF).legend(columns=2, position=nw) <1>
----------------------------------
<1> `.legend()` sets the position and style of the legend. In this example, `.legend(columns=2, position=nw)` places the legend in the north west position of the visualization with two columns.
@@ -210,7 +210,7 @@ Change how the data is displayed so that you can easily monitor the inbound traf
.es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.in.bytes).derivative() <1>
----------------------------------
-<1> `.derivative` plots the change in values over time.
+<1> `.derivative` plots the change in values over time.
[role="screenshot"]
image::images/timelion-math02.png[]
@@ -240,7 +240,7 @@ To make the visualization easier to analyze, change the data metric from bytes t
.es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.in.bytes).derivative().divide(1048576), .es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.out.bytes).derivative().multiply(-1).divide(1048576) <1>
----------------------------------
-<1> `.divide()` accepts the same input as `.multiply()`, then divides the data series by the defined divisor.
+<1> `.divide()` accepts the same input as `.multiply()`, then divides the data series by the defined divisor.
[role="screenshot"]
image::images/timelion-math04.png[]
@@ -256,7 +256,7 @@ Customize and format the visualization using functions:
----------------------------------
.es(index=metricbeat*,
timefield=@timestamp,
- metric=max:system.network.in.byte)
+ metric=max:system.network.in.bytes)
.derivative()
.divide(1048576)
.lines(fill=2, width=1)
@@ -270,7 +270,7 @@ Customize and format the visualization using functions:
.multiply(-1)
.divide(1048576)
.lines(fill=2, width=1) <3>
- .color(blue) < <4>
+ .color(blue) <4>
.label("Outbound traffic")
.legend(columns=2, position=nw) <5>
----------------------------------
diff --git a/package.json b/package.json
index 9707d3863d295..425527a058e86 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"clean": {
"extraPatterns": [
"build",
- "optimize",
+ "data/optimize",
"built_assets",
".eslintcache",
".node_binaries"
diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md
index 568980f50117d..5517dfa7f9a23 100644
--- a/src/core/MIGRATION_EXAMPLES.md
+++ b/src/core/MIGRATION_EXAMPLES.md
@@ -14,6 +14,7 @@ APIs to their New Platform equivalents.
- [3. New Platform shim using New Platform router](#3-new-platform-shim-using-new-platform-router)
- [4. New Platform plugin](#4-new-platform-plugin)
- [Accessing Services](#accessing-services)
+ - [Migrating Hapi "pre" handlers](#migrating-hapi-pre-handlers)
- [Chrome](#chrome)
- [Updating an application navlink](#updating-application-navlink)
- [Chromeless Applications](#chromeless-applications)
@@ -450,6 +451,142 @@ class Plugin {
}
```
+### Migrating Hapi "pre" handlers
+
+In the Legacy Platform, routes could provide a "pre" option in their config to
+register a function that should be run prior to the route handler. These
+"pre" handlers allow routes to share some business logic that may do some
+pre-work or validation. In Kibana, these are often used for license checks.
+
+The Kibana Platform's HTTP interface does not provide this functionality,
+however it is simple enough to port over using a higher-order function that can
+wrap the route handler.
+
+#### Simple example
+
+In this simple example, a pre-handler is used to either abort the request with
+an error or continue as normal. This is a simple "gate-keeping" pattern.
+
+```ts
+// Legacy pre-handler
+const licensePreRouting = (request) => {
+ const licenseInfo = getMyPluginLicenseInfo(request.server.plugins.xpack_main);
+ if (!licenseInfo.isOneOf(['gold', 'platinum', 'trial'])) {
+ throw Boom.forbidden(`You don't have the right license for MyPlugin!`);
+ }
+}
+
+server.route({
+ method: 'GET',
+ path: '/api/my-plugin/do-something',
+ config: {
+ pre: [{ method: licensePreRouting }]
+ },
+ handler: (req) => {
+ return doSomethingInteresting();
+ }
+})
+```
+
+In the Kibana Platform, the same functionality can be acheived by creating a
+function that takes a route handler (or factory for a route handler) as an
+argument and either invokes it in the successful case or returns an error
+response in the failure case.
+
+We'll call this a "high-order handler" similar to the "high-order component"
+pattern common in the React ecosystem.
+
+```ts
+// New Platform high-order handler
+const checkLicense =
(
+ handler: RequestHandler
+): RequestHandler
=> {
+ return (context, req, res) => {
+ const licenseInfo = getMyPluginLicenseInfo(context.licensing.license);
+
+ if (licenseInfo.hasAtLeast('gold')) {
+ return handler(context, req, res);
+ } else {
+ return res.forbidden({ body: `You don't have the right license for MyPlugin!` });
+ }
+ }
+}
+
+router.get(
+ { path: '/api/my-plugin/do-something', validate: false },
+ checkLicense(async (context, req, res) => {
+ const results = doSomethingInteresting();
+ return res.ok({ body: results });
+ }),
+)
+```
+
+#### Full Example
+
+In some cases, the route handler may need access to data that the pre-handler
+retrieves. In this case, you can utilize a handler _factory_ rather than a raw
+handler.
+
+```ts
+// Legacy pre-handler
+const licensePreRouting = (request) => {
+ const licenseInfo = getMyPluginLicenseInfo(request.server.plugins.xpack_main);
+ if (licenseInfo.isOneOf(['gold', 'platinum', 'trial'])) {
+ // In this case, the return value of the pre-handler is made available on
+ // whatever the 'assign' option is in the route config.
+ return licenseInfo;
+ } else {
+ // In this case, the route handler is never called and the user gets this
+ // error message
+ throw Boom.forbidden(`You don't have the right license for MyPlugin!`);
+ }
+}
+
+server.route({
+ method: 'GET',
+ path: '/api/my-plugin/do-something',
+ config: {
+ pre: [{ method: licensePreRouting, assign: 'licenseInfo' }]
+ },
+ handler: (req) => {
+ const licenseInfo = req.pre.licenseInfo;
+ return doSomethingInteresting(licenseInfo);
+ }
+})
+```
+
+In many cases, it may be simpler to duplicate the function call
+to retrieve the data again in the main handler. In this other cases, you can
+utilize a handler _factory_ rather than a raw handler as the argument to your
+high-order handler. This way the high-order handler can pass arbitrary arguments
+to the route handler.
+
+```ts
+// New Platform high-order handler
+const checkLicense =
(
+ handlerFactory: (licenseInfo: MyPluginLicenseInfo) => RequestHandler
+): RequestHandler
=> {
+ return (context, req, res) => {
+ const licenseInfo = getMyPluginLicenseInfo(context.licensing.license);
+
+ if (licenseInfo.hasAtLeast('gold')) {
+ const handler = handlerFactory(licenseInfo);
+ return handler(context, req, res);
+ } else {
+ return res.forbidden({ body: `You don't have the right license for MyPlugin!` });
+ }
+ }
+}
+
+router.get(
+ { path: '/api/my-plugin/do-something', validate: false },
+ checkLicense(licenseInfo => async (context, req, res) => {
+ const results = doSomethingInteresting(licenseInfo);
+ return res.ok({ body: results });
+ }),
+)
+```
+
## Chrome
In the Legacy Platform, the `ui/chrome` import contained APIs for a very wide
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
index 7a82ca0b1609c..34ba25f92beb6 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
@@ -18,6 +18,11 @@ kibana_vars=(
console.enabled
console.proxyConfig
console.proxyFilter
+ cpu.cgroup.path.override
+ cpuacct.cgroup.path.override
+ csp.rules
+ csp.strict
+ csp.warnLegacyBrowsers
elasticsearch.customHeaders
elasticsearch.hosts
elasticsearch.logQueries
@@ -30,6 +35,7 @@ kibana_vars=(
elasticsearch.sniffInterval
elasticsearch.sniffOnConnectionFault
elasticsearch.sniffOnStart
+ elasticsearch.ssl.alwaysPresentCertificate
elasticsearch.ssl.certificate
elasticsearch.ssl.certificateAuthorities
elasticsearch.ssl.key
@@ -42,9 +48,13 @@ kibana_vars=(
elasticsearch.startupTimeout
elasticsearch.username
i18n.locale
+ interpreter.enableInVisualize
+ kibana.autocompleteTerminateAfter
+ kibana.autocompleteTimeout
kibana.defaultAppId
kibana.index
logging.dest
+ logging.json
logging.quiet
logging.rotate.enabled
logging.rotate.everyBytes
@@ -55,18 +65,32 @@ kibana_vars=(
logging.useUTC
logging.verbose
map.includeElasticMapsService
+ map.proxyElasticMapsServiceInMaps
+ map.regionmap
+ map.tilemap.options.attribution
+ map.tilemap.options.maxZoom
+ map.tilemap.options.minZoom
+ map.tilemap.options.subdomains
+ map.tilemap.url
+ newsfeed.enabled
ops.interval
path.data
pid.file
regionmap
server.basePath
server.customResponseHeaders
+ server.compression.enabled
+ server.compression.referrerWhitelist
+ server.cors
+ server.cors.origin
server.defaultRoute
server.host
+ server.keepAliveTimeout
server.maxPayloadBytes
server.name
server.port
server.rewriteBasePath
+ server.socketTimeout
server.ssl.cert
server.ssl.certificate
server.ssl.certificateAuthorities
@@ -82,6 +106,7 @@ kibana_vars=(
server.ssl.truststore.password
server.ssl.redirectHttpFromPort
server.ssl.supportedProtocols
+ server.xsrf.disableProtection
server.xsrf.whitelist
status.allowAnonymous
status.v6ApiFormat
@@ -96,11 +121,13 @@ kibana_vars=(
xpack.apm.serviceMapEnabled
xpack.apm.ui.enabled
xpack.apm.ui.maxTraceItems
+ xpack.apm.ui.transactionGroupBucketSize
apm_oss.apmAgentConfigurationIndex
apm_oss.indexPattern
apm_oss.errorIndices
apm_oss.onboardingIndices
apm_oss.spanIndices
+ apm_oss.sourcemapIndices
apm_oss.transactionIndices
apm_oss.metricsIndices
xpack.canvas.enabled
@@ -116,6 +143,8 @@ kibana_vars=(
xpack.code.security.gitHostWhitelist
xpack.code.security.gitProtocolWhitelist
xpack.graph.enabled
+ xpack.graph.canEditDrillDownUrls
+ xpack.graph.savePolicy
xpack.grokdebugger.enabled
xpack.infra.enabled
xpack.infra.query.partitionFactor
@@ -128,11 +157,14 @@ kibana_vars=(
xpack.infra.sources.default.fields.timestamp
xpack.infra.sources.default.logAlias
xpack.infra.sources.default.metricAlias
+ xpack.license_management.enabled
xpack.ml.enabled
+ xpack.monitoring.cluster_alerts.email_notifications.email_address
xpack.monitoring.elasticsearch.password
xpack.monitoring.elasticsearch.pingTimeout
xpack.monitoring.elasticsearch.hosts
xpack.monitoring.elasticsearch.username
+ xpack.monitoring.elasticsearch.logFetchCount
xpack.monitoring.elasticsearch.ssl.certificateAuthorities
xpack.monitoring.elasticsearch.ssl.verificationMode
xpack.monitoring.enabled
@@ -166,6 +198,7 @@ kibana_vars=(
xpack.reporting.csv.maxSizeBytes
xpack.reporting.csv.scroll.duration
xpack.reporting.csv.scroll.size
+ xpack.reporting.capture.maxAttempts
xpack.reporting.enabled
xpack.reporting.encryptionKey
xpack.reporting.index
@@ -183,6 +216,8 @@ kibana_vars=(
xpack.reporting.queue.pollIntervalErrorMultiplier
xpack.reporting.queue.timeout
xpack.reporting.roles.allow
+ xpack.rollup.enabled
+ xpack.security.audit.enabled
xpack.searchprofiler.enabled
xpack.security.authc.providers
xpack.security.authc.oidc.realm
@@ -196,7 +231,10 @@ kibana_vars=(
xpack.security.session.idleTimeout
xpack.security.session.lifespan
xpack.security.loginAssistanceMessage
+ telemetry.allowChangingOptInStatus
telemetry.enabled
+ telemetry.optIn
+ telemetry.optInStatusUrl
telemetry.sendUsageFrom
)
diff --git a/src/plugins/data/common/es_query/kuery/types.ts b/src/plugins/data/common/es_query/kuery/types.ts
index 86cb7e08a767c..63c52bb64dc65 100644
--- a/src/plugins/data/common/es_query/kuery/types.ts
+++ b/src/plugins/data/common/es_query/kuery/types.ts
@@ -33,6 +33,8 @@ export interface KueryParseOptions {
startRule: string;
allowLeadingWildcards: boolean;
errorOnLuceneSyntax: boolean;
+ cursorSymbol?: string;
+ parseCursor?: boolean;
}
export { nodeTypes } from './node_types';
diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts
index 0527f833b0f8c..78bd2ec85f477 100644
--- a/src/plugins/data/public/autocomplete/autocomplete_service.ts
+++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts
@@ -75,3 +75,9 @@ export class AutocompleteService {
this.querySuggestionProviders.clear();
}
}
+
+/** @public **/
+export type AutocompleteSetup = ReturnType;
+
+/** @public **/
+export type AutocompleteStart = ReturnType;
diff --git a/src/plugins/data/public/autocomplete/index.ts b/src/plugins/data/public/autocomplete/index.ts
index 5b8f3ae510bfd..c2b21e84b7a38 100644
--- a/src/plugins/data/public/autocomplete/index.ts
+++ b/src/plugins/data/public/autocomplete/index.ts
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
+import * as autocomplete from './static';
+export { AutocompleteService, AutocompleteSetup, AutocompleteStart } from './autocomplete_service';
-export { AutocompleteService } from './autocomplete_service';
-export { QuerySuggestion, QuerySuggestionType, QuerySuggestionsGetFn } from './types';
+export { autocomplete };
diff --git a/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts
index 53abdd44c0c3f..94054ed56f42a 100644
--- a/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts
+++ b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts
@@ -19,13 +19,20 @@
import { IFieldType, IIndexPattern } from '../../../common/index_patterns';
-export type QuerySuggestionType = 'field' | 'value' | 'operator' | 'conjunction' | 'recentSearch';
+export enum QuerySuggestionsTypes {
+ Field = 'field',
+ Value = 'value',
+ Operator = 'operator',
+ Conjunction = 'conjunction',
+ RecentSearch = 'recentSearch',
+}
export type QuerySuggestionsGetFn = (
args: QuerySuggestionsGetFnArgs
) => Promise | undefined;
-interface QuerySuggestionsGetFnArgs {
+/** @public **/
+export interface QuerySuggestionsGetFnArgs {
language: string;
indexPatterns: IIndexPattern[];
query: string;
@@ -35,22 +42,21 @@ interface QuerySuggestionsGetFnArgs {
boolFilter?: any;
}
-interface BasicQuerySuggestion {
- type: QuerySuggestionType;
- description?: string;
+/** @public **/
+export interface BasicQuerySuggestion {
+ type: QuerySuggestionsTypes;
+ description?: string | JSX.Element;
end: number;
start: number;
text: string;
cursorIndex?: number;
}
-interface FieldQuerySuggestion extends BasicQuerySuggestion {
- type: 'field';
+/** @public **/
+export interface FieldQuerySuggestion extends BasicQuerySuggestion {
+ type: QuerySuggestionsTypes.Field;
field: IFieldType;
}
-// A union type allows us to do easy type guards in the code. For example, if I want to ensure I'm
-// working with a FieldAutocompleteSuggestion, I can just do `if ('field' in suggestion)` and the
-// TypeScript compiler will narrow the type to the parts of the union that have a field prop.
/** @public **/
export type QuerySuggestion = BasicQuerySuggestion | FieldQuerySuggestion;
diff --git a/src/plugins/data/public/autocomplete/types.ts b/src/plugins/data/public/autocomplete/static.ts
similarity index 76%
rename from src/plugins/data/public/autocomplete/types.ts
rename to src/plugins/data/public/autocomplete/static.ts
index 759e2dd25a5bc..7d627486c6d65 100644
--- a/src/plugins/data/public/autocomplete/types.ts
+++ b/src/plugins/data/public/autocomplete/static.ts
@@ -17,17 +17,11 @@
* under the License.
*/
-import { AutocompleteService } from './autocomplete_service';
-
-/** @public **/
-export type AutocompleteSetup = ReturnType;
-
-/** @public **/
-export type AutocompleteStart = ReturnType;
-
-/** @public **/
export {
QuerySuggestion,
+ QuerySuggestionsTypes,
QuerySuggestionsGetFn,
- QuerySuggestionType,
+ QuerySuggestionsGetFnArgs,
+ BasicQuerySuggestion,
+ FieldQuerySuggestion,
} from './providers/query_suggestion_provider';
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index bc25c64f0e96e..2fa6b8deae69d 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -18,7 +18,6 @@
*/
import { PluginInitializerContext } from '../../../core/public';
-import * as autocomplete from './autocomplete';
export function plugin(initializerContext: PluginInitializerContext) {
return new DataPublicPlugin(initializerContext);
@@ -44,7 +43,7 @@ export {
RefreshInterval,
TimeRange,
} from '../common';
-
+export { autocomplete } from './autocomplete';
export * from './field_formats';
export * from './index_patterns';
export * from './search';
@@ -70,5 +69,3 @@ export {
// Export plugin after all other imports
import { DataPublicPlugin } from './plugin';
export { DataPublicPlugin as Plugin };
-
-export { autocomplete };
diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts
index 1ade48b3537c8..8c790ac2ddc46 100644
--- a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts
+++ b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts
@@ -29,9 +29,16 @@ import { esFilters } from '../../../common';
import { coreMock } from '../../../../../core/public/mocks';
const setupMock = coreMock.createSetup();
-setupMock.uiSettings.get.mockImplementation((key: string) => {
- return true;
-});
+const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => {
+ switch (key) {
+ case 'filters:pinnedByDefault':
+ return pinnedByDefault;
+ default:
+ throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`);
+ }
+};
+
+setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true));
describe('filter_manager', () => {
let updateSubscription: Subscription | undefined;
@@ -224,6 +231,44 @@ describe('filter_manager', () => {
expect(newGlobalFilters).toHaveLength(2);
expect(newAppFilters).toHaveLength(1);
});
+
+ test('set filter with no state, and force pin', async () => {
+ const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 38);
+ f1.$state = undefined;
+
+ filterManager.setFilters([f1], true);
+ expect(filterManager.getGlobalFilters()).toHaveLength(1);
+ expect(filterManager.getAppFilters()).toHaveLength(0);
+ });
+
+ test('set filter with no state, and no pin', async () => {
+ const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 38);
+ f1.$state = undefined;
+
+ filterManager.setFilters([f1], false);
+ expect(filterManager.getGlobalFilters()).toHaveLength(0);
+ expect(filterManager.getAppFilters()).toHaveLength(1);
+ });
+
+ test('set filters with default pin', async () => {
+ const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 38);
+ f1.$state = undefined;
+ setupMock.uiSettings.get.mockImplementationOnce(uiSettingsMock(true));
+
+ filterManager.setFilters([f1]);
+ expect(filterManager.getGlobalFilters()).toHaveLength(1);
+ expect(filterManager.getAppFilters()).toHaveLength(0);
+ });
+
+ test('set filters without default pin', async () => {
+ const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 38);
+ f1.$state = undefined;
+
+ setupMock.uiSettings.get.mockImplementationOnce(uiSettingsMock(false));
+ filterManager.setFilters([f1]);
+ expect(filterManager.getGlobalFilters()).toHaveLength(0);
+ expect(filterManager.getAppFilters()).toHaveLength(1);
+ });
});
describe('add filters', () => {
diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts
index 6c5cdbaffce5e..aa77f10d89f63 100644
--- a/src/plugins/data/public/query/filter_manager/filter_manager.ts
+++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts
@@ -124,7 +124,10 @@ export class FilterManager {
/* Setters */
- public addFilters(filters: esFilters.Filter[] | esFilters.Filter, pinFilterStatus?: boolean) {
+ public addFilters(
+ filters: esFilters.Filter[] | esFilters.Filter,
+ pinFilterStatus: boolean = this.uiSettings.get('filters:pinnedByDefault')
+ ) {
if (!Array.isArray(filters)) {
filters = [filters];
}
@@ -133,12 +136,6 @@ export class FilterManager {
return;
}
- if (pinFilterStatus === undefined) {
- pinFilterStatus = this.uiSettings.get('filters:pinnedByDefault');
- }
-
- // Set the store of all filters. For now.
- // In the future, all filters should come in with filter state store already set.
const store = pinFilterStatus
? esFilters.FilterStateStore.GLOBAL_STATE
: esFilters.FilterStateStore.APP_STATE;
@@ -157,7 +154,16 @@ export class FilterManager {
this.handleStateUpdate(newFilters);
}
- public setFilters(newFilters: esFilters.Filter[]) {
+ public setFilters(
+ newFilters: esFilters.Filter[],
+ pinFilterStatus: boolean = this.uiSettings.get('filters:pinnedByDefault')
+ ) {
+ const store = pinFilterStatus
+ ? esFilters.FilterStateStore.GLOBAL_STATE
+ : esFilters.FilterStateStore.APP_STATE;
+
+ FilterManager.setFiltersStore(newFilters, store);
+
const mappedFilters = mapAndFlattenFilters(newFilters);
const newPartitionedFilters = FilterManager.partitionFilters(mappedFilters);
const mergedFilters = this.mergeIncomingFilters(newPartitionedFilters);
diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts
index 6b6ff5e62e63f..e62aba5f2713d 100644
--- a/src/plugins/data/public/types.ts
+++ b/src/plugins/data/public/types.ts
@@ -20,7 +20,7 @@
import { CoreStart } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { IUiActionsSetup, IUiActionsStart } from 'src/plugins/ui_actions/public';
-import { AutocompleteSetup, AutocompleteStart } from './autocomplete/types';
+import { AutocompleteSetup, AutocompleteStart } from './autocomplete';
import { FieldFormatsSetup, FieldFormatsStart } from './field_formats';
import { ISearchSetup, ISearchStart } from './search';
import { QuerySetup, QueryStart } from './query';
diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
index cf219c35bcced..7a8c0f7269fa1 100644
--- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
+++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
@@ -89,8 +89,6 @@ const KEY_CODES = {
END: 35,
};
-const recentSearchType: autocomplete.QuerySuggestionType = 'recentSearch';
-
export class QueryStringInputUI extends Component {
public state: State = {
isSuggestionsVisible: false,
@@ -193,7 +191,7 @@ export class QueryStringInputUI extends Component {
const text = toUser(recentSearch);
const start = 0;
const end = query.length;
- return { type: recentSearchType, text, start, end };
+ return { type: autocomplete.QuerySuggestionsTypes.RecentSearch, text, start, end };
});
};
@@ -343,7 +341,7 @@ export class QueryStringInputUI extends Component {
selectionEnd: start + (cursorIndex ? cursorIndex : text.length),
});
- if (type === recentSearchType) {
+ if (type === autocomplete.QuerySuggestionsTypes.RecentSearch) {
this.setState({ isSuggestionsVisible: false, index: null });
this.onSubmit({ query: newQueryString, language: this.props.query.language });
}
diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx
index 0c5c701642757..ba92be8947ea5 100644
--- a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx
+++ b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx
@@ -31,7 +31,7 @@ const mockSuggestion: autocomplete.QuerySuggestion = {
end: 0,
start: 42,
text: 'as promised, not helpful',
- type: 'value',
+ type: autocomplete.QuerySuggestionsTypes.Value,
};
describe('SuggestionComponent', () => {
diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx
index b84f612b6d13a..eebe438025949 100644
--- a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx
+++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx
@@ -33,14 +33,14 @@ const mockSuggestions: autocomplete.QuerySuggestion[] = [
end: 0,
start: 42,
text: 'as promised, not helpful',
- type: 'value',
+ type: autocomplete.QuerySuggestionsTypes.Value,
},
{
description: 'This is another unhelpful suggestion',
end: 0,
start: 42,
text: 'yep',
- type: 'field',
+ type: autocomplete.QuerySuggestionsTypes.Field,
},
];
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx
index 5b7ddc2b450d6..07ea97f442b7f 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx
@@ -4,12 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useContext, useState, useEffect } from 'react';
import { EuiButtonIcon, EuiPanel } from '@elastic/eui';
import theme from '@elastic/eui/dist/eui_theme_light.json';
-import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
+import React, { useContext, useEffect, useState } from 'react';
+import styled from 'styled-components';
import { CytoscapeContext } from './Cytoscape';
+import { animationOptions, nodeHeight } from './cytoscapeOptions';
import { FullscreenPanel } from './FullscreenPanel';
const ControlsContainer = styled('div')`
@@ -58,6 +59,17 @@ export function Controls() {
}
}, [cy]);
+ function center() {
+ if (cy) {
+ const eles = cy.nodes();
+ cy.animate({
+ ...animationOptions,
+ center: { eles },
+ fit: { eles, padding: nodeHeight }
+ });
+ }
+ }
+
function zoomIn() {
doZoom(cy, increment);
}
@@ -82,6 +94,9 @@ export function Controls() {
const zoomOutLabel = i18n.translate('xpack.apm.serviceMap.zoomOut', {
defaultMessage: 'Zoom out'
});
+ const centerLabel = i18n.translate('xpack.apm.serviceMap.center', {
+ defaultMessage: 'Center'
+ });
return (
@@ -103,6 +118,15 @@ export function Controls() {
title={zoomOutLabel}
/>
+
+
+
);
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
index a243021ddc5fd..f87f59f64ec70 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
@@ -7,13 +7,23 @@ import theme from '@elastic/eui/dist/eui_theme_light.json';
import cytoscape from 'cytoscape';
import { defaultIcon, iconForNode } from './icons';
+export const animationOptions: cytoscape.AnimationOptions = {
+ duration: parseInt(theme.euiAnimSpeedNormal, 10),
+ // @ts-ignore The cubic-bezier options here are not recognized by the cytoscape types
+ easing: theme.euiAnimSlightBounce
+};
+
+export const nodeHeight = parseInt(theme.avatarSizing.l.size, 10);
+
const layout = {
name: 'dagre',
nodeDimensionsIncludeLabels: true,
rankDir: 'LR',
animate: true,
- animationEasing: theme.euiAnimSlightBounce,
- animationDuration: parseInt(theme.euiAnimSpeedNormal, 10)
+ animationEasing: animationOptions.easing,
+ animationDuration: animationOptions.duration,
+ fit: true,
+ padding: nodeHeight
};
function isService(el: cytoscape.NodeSingular) {
@@ -45,7 +55,7 @@ const style: cytoscape.Stylesheet[] = [
// specifying a subset of the fonts for the label text.
'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif',
'font-size': theme.euiFontSizeXS,
- height: theme.avatarSizing.l.size,
+ height: nodeHeight,
label: 'data(label)',
'min-zoomed-font-size': theme.euiSizeL,
'overlay-opacity': 0,
@@ -56,7 +66,7 @@ const style: cytoscape.Stylesheet[] = [
'text-background-padding': theme.paddingSizes.xs,
'text-background-shape': 'roundrectangle',
'text-margin-y': theme.paddingSizes.s,
- 'text-max-width': '85px',
+ 'text-max-width': '200px',
'text-valign': 'bottom',
'text-wrap': 'ellipsis',
width: theme.avatarSizing.l.size
diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts
index 39611dd6c2994..7f81adad6bf9b 100644
--- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts
+++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts
@@ -6,7 +6,7 @@
import { connect } from 'react-redux';
import { compose, withProps } from 'recompose';
-import { jobCompletionNotifications } from '../../../../../reporting/public/lib/job_completion_notifications';
+import * as jobCompletionNotifications from '../../../../../reporting/public/lib/job_completion_notifications';
// @ts-ignore Untyped local
import { getWorkpad, getPages } from '../../../state/selectors/workpad';
// @ts-ignore Untyped local
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_logistics.tsx b/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_logistics.tsx
index 290ade3504551..d2134837c15e5 100644
--- a/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_logistics.tsx
+++ b/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_logistics.tsx
@@ -18,7 +18,7 @@ import {
} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/components';
import { documentationService } from '../../../services/documentation';
import { StepProps } from '../types';
-import { schemas } from '../template_form_schemas';
+import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas';
// Create or Form components with partial props that are common to all instances
const UseField = getUseField({ component: Field });
@@ -131,6 +131,7 @@ export const StepLogistics: React.FunctionComponent = ({
['data-test-subj']: name.testSubject,
euiFieldProps: { disabled: isEditing },
}}
+ config={isEditing ? nameConfigWithoutValidations : nameConfig}
/>
{/* Index patterns */}
diff --git a/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_form_schemas.tsx b/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_form_schemas.tsx
index 5f3e28ddffb8e..ed2616cc64e38 100644
--- a/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_form_schemas.tsx
+++ b/x-pack/legacy/plugins/index_management/public/app/components/template_form/template_form_schemas.tsx
@@ -12,6 +12,7 @@ import {
FormSchema,
FIELD_TYPES,
VALIDATION_TYPES,
+ FieldConfig,
} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
import {
@@ -34,70 +35,71 @@ const {
const { toInt } = fieldFormatters;
const indexPatternInvalidCharacters = INVALID_INDEX_PATTERN_CHARS.join(' ');
-export const schemas: Record = {
- logistics: {
- name: {
- type: FIELD_TYPES.TEXT,
- label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldNameLabel', {
- defaultMessage: 'Name',
+export const nameConfig: FieldConfig = {
+ type: FIELD_TYPES.TEXT,
+ label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldNameLabel', {
+ defaultMessage: 'Name',
+ }),
+ validations: [
+ {
+ validator: emptyField(
+ i18n.translate('xpack.idxMgmt.templateValidation.templateNameRequiredError', {
+ defaultMessage: 'A template name is required.',
+ })
+ ),
+ },
+ {
+ validator: containsCharsField({
+ chars: ' ',
+ message: i18n.translate('xpack.idxMgmt.templateValidation.templateNameSpacesError', {
+ defaultMessage: 'Spaces are not allowed in a template name.',
+ }),
}),
- validations: [
- {
- validator: emptyField(
- i18n.translate('xpack.idxMgmt.templateValidation.templateNameRequiredError', {
- defaultMessage: 'A template name is required.',
- })
- ),
- },
- {
- validator: containsCharsField({
- chars: ' ',
- message: i18n.translate('xpack.idxMgmt.templateValidation.templateNameSpacesError', {
- defaultMessage: 'Spaces are not allowed in a template name.',
- }),
- }),
- },
- {
- validator: startsWithField({
- char: '_',
- message: i18n.translate(
- 'xpack.idxMgmt.templateValidation.templateNameUnderscoreError',
- {
- defaultMessage: 'A template name must not start with an underscore.',
- }
- ),
- }),
- },
- {
- validator: startsWithField({
- char: '.',
- message: i18n.translate('xpack.idxMgmt.templateValidation.templateNamePeriodError', {
- defaultMessage: 'A template name must not start with a period.',
- }),
- }),
- },
- {
- validator: containsCharsField({
- chars: INVALID_TEMPLATE_NAME_CHARS,
- message: ({ charsFound }) =>
- i18n.translate(
- 'xpack.idxMgmt.templateValidation.templateNameInvalidaCharacterError',
- {
- defaultMessage: 'A template name must not contain the character "{invalidChar}"',
- values: { invalidChar: charsFound[0] },
- }
- ),
+ },
+ {
+ validator: startsWithField({
+ char: '_',
+ message: i18n.translate('xpack.idxMgmt.templateValidation.templateNameUnderscoreError', {
+ defaultMessage: 'A template name must not start with an underscore.',
+ }),
+ }),
+ },
+ {
+ validator: startsWithField({
+ char: '.',
+ message: i18n.translate('xpack.idxMgmt.templateValidation.templateNamePeriodError', {
+ defaultMessage: 'A template name must not start with a period.',
+ }),
+ }),
+ },
+ {
+ validator: containsCharsField({
+ chars: INVALID_TEMPLATE_NAME_CHARS,
+ message: ({ charsFound }) =>
+ i18n.translate('xpack.idxMgmt.templateValidation.templateNameInvalidaCharacterError', {
+ defaultMessage: 'A template name must not contain the character "{invalidChar}"',
+ values: { invalidChar: charsFound[0] },
}),
- },
- {
- validator: lowerCaseStringField(
- i18n.translate('xpack.idxMgmt.templateValidation.templateNameLowerCaseRequiredError', {
- defaultMessage: 'The template name must be in lowercase.',
- })
- ),
- },
- ],
+ }),
},
+ {
+ validator: lowerCaseStringField(
+ i18n.translate('xpack.idxMgmt.templateValidation.templateNameLowerCaseRequiredError', {
+ defaultMessage: 'The template name must be in lowercase.',
+ })
+ ),
+ },
+ ],
+};
+
+export const nameConfigWithoutValidations: FieldConfig = {
+ ...nameConfig,
+ validations: [],
+};
+
+export const schemas: Record = {
+ logistics: {
+ name: nameConfig,
indexPatterns: {
type: FIELD_TYPES.COMBO_BOX,
defaultValue: [],
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/conjunction.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/conjunction.js
deleted file mode 100644
index 94990edef5e82..0000000000000
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/conjunction.js
+++ /dev/null
@@ -1,48 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import expect from '@kbn/expect';
-import { getSuggestionsProvider } from '../conjunction';
-
-describe('Kuery conjunction suggestions', function() {
- const getSuggestions = getSuggestionsProvider();
-
- it('should return a function', function() {
- expect(typeof getSuggestions).to.be('function');
- });
-
- it('should not suggest anything for phrases not ending in whitespace', function() {
- const text = 'foo';
- const suggestions = getSuggestions({ text });
- expect(suggestions).to.eql([]);
- });
-
- it('should suggest and/or for phrases ending in whitespace', function() {
- const text = 'foo ';
- const suggestions = getSuggestions({ text });
- expect(suggestions.length).to.be(2);
- expect(suggestions.map(suggestion => suggestion.text)).to.eql(['and ', 'or ']);
- });
-
- it('should suggest to insert the suggestion at the end of the string', function() {
- const text = 'bar ';
- const end = text.length;
- const suggestions = getSuggestions({ text, end });
- expect(suggestions.length).to.be(2);
- expect(suggestions.map(suggestion => suggestion.start)).to.eql([end, end]);
- expect(suggestions.map(suggestion => suggestion.end)).to.eql([end, end]);
- });
- it('should have descriptions', function() {
- const text = ' ';
- const suggestions = getSuggestions({ text });
- expect(typeof suggestions).to.be('object');
- expect(Object.keys(suggestions).length).to.be(2);
- suggestions.forEach(suggestion => {
- expect(typeof suggestion).to.be('object');
- expect(suggestion).to.have.property('description');
- });
- });
-});
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/field.js
deleted file mode 100644
index 6dc07da68a5ea..0000000000000
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/field.js
+++ /dev/null
@@ -1,146 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import expect from '@kbn/expect';
-import { getSuggestionsProvider } from '../field';
-import indexPatternResponse from '../__fixtures__/index_pattern_response.json';
-import { isFilterable } from '../../../../../../../src/plugins/data/public';
-
-describe('Kuery field suggestions', function() {
- let indexPattern;
- let indexPatterns;
- let getSuggestions;
-
- beforeEach(() => {
- indexPattern = indexPatternResponse;
- indexPatterns = [indexPattern];
- getSuggestions = getSuggestionsProvider({ indexPatterns });
- });
-
- it('should return a function', function() {
- expect(typeof getSuggestions).to.be('function');
- });
-
- it('should return filterable fields', function() {
- const prefix = '';
- const suffix = '';
- const suggestions = getSuggestions({ prefix, suffix });
- const filterableFields = indexPattern.fields.filter(isFilterable);
- expect(suggestions.length).to.be(filterableFields.length);
- });
-
- it('should filter suggestions based on the query', () => {
- const prefix = 'machine';
- const suffix = '';
- const suggestions = getSuggestions({ prefix, suffix });
- expect(suggestions.find(({ text }) => text === 'machine.os ')).to.be.ok();
- });
-
- it('should filter suggestions case insensitively', () => {
- const prefix = 'MACHINE';
- const suffix = '';
- const suggestions = getSuggestions({ prefix, suffix });
- expect(suggestions.find(({ text }) => text === 'machine.os ')).to.be.ok();
- });
-
- it('should return suggestions where the query matches somewhere in the middle', () => {
- const prefix = '.';
- const suffix = '';
- const suggestions = getSuggestions({ prefix, suffix });
- expect(suggestions.find(({ text }) => text === 'machine.os ')).to.be.ok();
- });
-
- it('should return field names that start with the query first', () => {
- const prefix = 'e';
- const suffix = '';
- const suggestions = getSuggestions({ prefix, suffix });
- const extensionIndex = suggestions.findIndex(({ text }) => text === 'extension ');
- const bytesIndex = suggestions.findIndex(({ text }) => text === 'bytes ');
- expect(extensionIndex).to.be.lessThan(bytesIndex);
- });
-
- it('should sort keyword fields before analyzed versions', () => {
- const prefix = '';
- const suffix = '';
- const suggestions = getSuggestions({ prefix, suffix });
- const analyzedIndex = suggestions.findIndex(({ text }) => text === 'machine.os ');
- const keywordIndex = suggestions.findIndex(({ text }) => text === 'machine.os.raw ');
- expect(keywordIndex).to.be.lessThan(analyzedIndex);
- });
-
- it('should have descriptions', function() {
- const prefix = '';
- const suffix = '';
- const suggestions = getSuggestions({ prefix, suffix });
- expect(suggestions.length).to.be.greaterThan(0);
- suggestions.forEach(suggestion => {
- expect(suggestion).to.have.property('description');
- });
- });
-
- describe('nested fields', function() {
- it("should automatically wrap nested fields in KQL's nested syntax", () => {
- const prefix = 'ch';
- const suffix = '';
- const suggestions = getSuggestions({ prefix, suffix });
-
- const suggestion = suggestions.find(({ field }) => field.name === 'nestedField.child');
- expect(suggestion.text).to.be('nestedField:{ child }');
-
- // For most suggestions the cursor can be placed at the end of the suggestion text, but
- // for the nested field syntax we want to place the cursor inside the curly braces
- expect(suggestion.cursorIndex).to.be(20);
- });
-
- it('should narrow suggestions to children of a nested path if provided', () => {
- const prefix = 'ch';
- const suffix = '';
-
- const allSuggestions = getSuggestions({ prefix, suffix });
- expect(allSuggestions.length).to.be.greaterThan(2);
-
- const nestedSuggestions = getSuggestions({ prefix, suffix, nestedPath: 'nestedField' });
- expect(nestedSuggestions).to.have.length(2);
- });
-
- it("should not wrap the suggestion in KQL's nested syntax if the correct nested path is already provided", () => {
- const prefix = 'ch';
- const suffix = '';
-
- const suggestions = getSuggestions({ prefix, suffix, nestedPath: 'nestedField' });
- const suggestion = suggestions.find(({ field }) => field.name === 'nestedField.child');
- expect(suggestion.text).to.be('child ');
- });
-
- it('should handle fields nested multiple levels deep', () => {
- const prefix = 'doubly';
- const suffix = '';
-
- const suggestionsWithNoPath = getSuggestions({ prefix, suffix });
- expect(suggestionsWithNoPath).to.have.length(1);
- const [noPathSuggestion] = suggestionsWithNoPath;
- expect(noPathSuggestion.text).to.be('nestedField.nestedChild:{ doublyNestedChild }');
-
- const suggestionsWithPartialPath = getSuggestions({
- prefix,
- suffix,
- nestedPath: 'nestedField',
- });
- expect(suggestionsWithPartialPath).to.have.length(1);
- const [partialPathSuggestion] = suggestionsWithPartialPath;
- expect(partialPathSuggestion.text).to.be('nestedChild:{ doublyNestedChild }');
-
- const suggestionsWithFullPath = getSuggestions({
- prefix,
- suffix,
- nestedPath: 'nestedField.nestedChild',
- });
- expect(suggestionsWithFullPath).to.have.length(1);
- const [fullPathSuggestion] = suggestionsWithFullPath;
- expect(fullPathSuggestion.text).to.be('doublyNestedChild ');
- });
- });
-});
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/operator.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/operator.js
deleted file mode 100644
index c248e3e8366a9..0000000000000
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/operator.js
+++ /dev/null
@@ -1,64 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import expect from '@kbn/expect';
-import { getSuggestionsProvider } from '../operator';
-import indexPatternResponse from '../__fixtures__/index_pattern_response.json';
-
-describe('Kuery operator suggestions', function() {
- let indexPatterns;
- let getSuggestions;
-
- beforeEach(() => {
- indexPatterns = [indexPatternResponse];
- getSuggestions = getSuggestionsProvider({ indexPatterns });
- });
-
- it('should return a function', function() {
- expect(typeof getSuggestions).to.be('function');
- });
-
- it('should not return suggestions for non-fields', () => {
- const fieldName = 'foo';
- const suggestions = getSuggestions({ fieldName });
- expect(suggestions.length).to.eql([]);
- });
-
- it('should return exists for every field', () => {
- const fieldName = 'custom_user_field';
- const suggestions = getSuggestions({ fieldName });
- expect(suggestions.length).to.eql(1);
- expect(suggestions[0].text).to.be(': * ');
- });
-
- it('should return equals for string fields', () => {
- const fieldName = 'machine.os';
- const suggestions = getSuggestions({ fieldName });
- expect(suggestions.find(({ text }) => text === ': ')).to.be.ok();
- expect(suggestions.find(({ text }) => text === '< ')).to.not.be.ok();
- });
-
- it('should return numeric operators for numeric fields', () => {
- const fieldName = 'bytes';
- const suggestions = getSuggestions({ fieldName });
- expect(suggestions.find(({ text }) => text === ': ')).to.be.ok();
- expect(suggestions.find(({ text }) => text === '< ')).to.be.ok();
- });
-
- it('should have descriptions', function() {
- const fieldName = 'bytes';
- const suggestions = getSuggestions({ fieldName });
- expect(suggestions.length).to.be.greaterThan(0);
- suggestions.forEach(suggestion => {
- expect(suggestion).to.have.property('description');
- });
- });
-
- it('should handle nested paths', () => {
- const suggestions = getSuggestions({ fieldName: 'child', nestedPath: 'nestedField' });
- expect(suggestions.length).to.be.greaterThan(0);
- });
-});
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.test.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.test.ts
new file mode 100644
index 0000000000000..e8aec0deec6d7
--- /dev/null
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.test.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { setupGetConjunctionSuggestions } from './conjunction';
+import { autocomplete, esKuery } from '../../../../../../src/plugins/data/public';
+import { coreMock } from '../../../../../../src/core/public/mocks';
+
+const mockKueryNode = (kueryNode: Partial) =>
+ (kueryNode as unknown) as esKuery.KueryNode;
+
+describe('Kuery conjunction suggestions', () => {
+ const querySuggestionsArgs = (null as unknown) as autocomplete.QuerySuggestionsGetFnArgs;
+ let getSuggestions: ReturnType;
+
+ beforeEach(() => {
+ getSuggestions = setupGetConjunctionSuggestions(coreMock.createSetup());
+ });
+
+ test('should return a function', () => {
+ expect(typeof getSuggestions).toBe('function');
+ });
+
+ test('should not suggest anything for phrases not ending in whitespace', async () => {
+ const text = 'foo';
+ const suggestions = await getSuggestions(querySuggestionsArgs, mockKueryNode({ text }));
+
+ expect(suggestions).toEqual([]);
+ });
+
+ test('should suggest and/or for phrases ending in whitespace', async () => {
+ const text = 'foo ';
+ const suggestions = await getSuggestions(querySuggestionsArgs, mockKueryNode({ text }));
+
+ expect(suggestions.length).toBe(2);
+ expect(suggestions.map(suggestion => suggestion.text)).toEqual(['and ', 'or ']);
+ });
+
+ test('should suggest to insert the suggestion at the end of the string', async () => {
+ const text = 'bar ';
+ const end = text.length;
+ const suggestions = await getSuggestions(querySuggestionsArgs, mockKueryNode({ text, end }));
+
+ expect(suggestions.length).toBe(2);
+ expect(suggestions.map(suggestion => suggestion.start)).toEqual([end, end]);
+ expect(suggestions.map(suggestion => suggestion.end)).toEqual([end, end]);
+ });
+
+ test('should have descriptions', async () => {
+ const text = ' ';
+ const suggestions = await getSuggestions(querySuggestionsArgs, mockKueryNode({ text }));
+
+ expect(typeof suggestions).toBe('object');
+ expect(Object.keys(suggestions).length).toBe(2);
+
+ suggestions.forEach(suggestion => {
+ expect(typeof suggestion).toBe('object');
+ expect(suggestion).toHaveProperty('description');
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.tsx
similarity index 72%
rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.js
rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.tsx
index 66f4e6f8eb341..f570586274fdd 100644
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.js
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.tsx
@@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
+import { $Keys } from 'utility-types';
import { FormattedMessage } from '@kbn/i18n/react';
-
-const type = 'conjunction';
+import { KqlQuerySuggestionProvider } from './types';
+import { autocomplete } from '../../../../../../src/plugins/data/public';
const bothArgumentsText = (
);
-const conjunctions = {
+const conjunctions: Record = {
and: (
{
+ return (querySuggestionsArgs, { text, end }) => {
+ let suggestions: autocomplete.QuerySuggestion[] | [] = [];
+
+ if (text.endsWith(' ')) {
+ suggestions = Object.keys(conjunctions).map((key: $Keys) => ({
+ type: autocomplete.QuerySuggestionsTypes.Conjunction,
+ text: `${key} `,
+ description: conjunctions[key],
+ start: end,
+ end,
+ }));
+ }
-export function getSuggestionsProvider() {
- return function getConjunctionSuggestions({ text, end }) {
- if (!text.endsWith(' ')) return [];
- const suggestions = Object.keys(conjunctions).map(conjunction => {
- const text = `${conjunction} `;
- const description = getDescription(conjunction);
- return { type, text, description, start: end, end };
- });
- return suggestions;
+ return Promise.resolve(suggestions);
};
-}
+};
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.test.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.test.ts
new file mode 100644
index 0000000000000..2fd5cfd17eb69
--- /dev/null
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.test.ts
@@ -0,0 +1,216 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import indexPatternResponse from './__fixtures__/index_pattern_response.json';
+
+import { setupGetFieldSuggestions } from './field';
+import { isFilterable, autocomplete, esKuery } from '../../../../../../src/plugins/data/public';
+import { coreMock } from '../../../../../../src/core/public/mocks';
+
+const mockKueryNode = (kueryNode: Partial) =>
+ (kueryNode as unknown) as esKuery.KueryNode;
+
+describe('Kuery field suggestions', () => {
+ let querySuggestionsArgs: autocomplete.QuerySuggestionsGetFnArgs;
+ let getSuggestions: ReturnType;
+
+ beforeEach(() => {
+ querySuggestionsArgs = ({
+ indexPatterns: [indexPatternResponse],
+ } as unknown) as autocomplete.QuerySuggestionsGetFnArgs;
+
+ getSuggestions = setupGetFieldSuggestions(coreMock.createSetup());
+ });
+
+ test('should return a function', () => {
+ expect(typeof getSuggestions).toBe('function');
+ });
+
+ test('should return filterable fields', async () => {
+ const prefix = '';
+ const suffix = '';
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ prefix, suffix })
+ );
+ const filterableFields = indexPatternResponse.fields.filter(isFilterable);
+
+ expect(suggestions.length).toBe(filterableFields.length);
+ });
+
+ test('should filter suggestions based on the query', async () => {
+ const prefix = 'machine';
+ const suffix = '';
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ prefix, suffix })
+ );
+
+ expect(suggestions.find(({ text }) => text === 'machine.os ')).toBeDefined();
+ });
+
+ test('should filter suggestions case insensitively', async () => {
+ const prefix = 'MACHINE';
+ const suffix = '';
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ prefix, suffix })
+ );
+
+ expect(suggestions.find(({ text }) => text === 'machine.os ')).toBeDefined();
+ });
+
+ test('should return suggestions where the query matches somewhere in the middle', async () => {
+ const prefix = '.';
+ const suffix = '';
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ prefix, suffix })
+ );
+
+ expect(suggestions.find(({ text }) => text === 'machine.os ')).toBeDefined();
+ });
+
+ test('should return field names that start with the query first', async () => {
+ const prefix = 'e';
+ const suffix = '';
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ prefix, suffix })
+ );
+ const extensionIndex = suggestions.findIndex(({ text }) => text === 'extension ');
+ const bytesIndex = suggestions.findIndex(({ text }) => text === 'bytes ');
+
+ expect(extensionIndex).toBeLessThan(bytesIndex);
+ });
+
+ test('should sort keyword fields before analyzed versions', async () => {
+ const prefix = '';
+ const suffix = '';
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ prefix, suffix })
+ );
+ const analyzedIndex = suggestions.findIndex(({ text }) => text === 'machine.os ');
+ const keywordIndex = suggestions.findIndex(({ text }) => text === 'machine.os.raw ');
+
+ expect(keywordIndex).toBeLessThan(analyzedIndex);
+ });
+
+ test('should have descriptions', async () => {
+ const prefix = '';
+ const suffix = '';
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ prefix, suffix })
+ );
+ expect(suggestions.length).toBeGreaterThan(0);
+ suggestions.forEach(suggestion => {
+ expect(suggestion).toHaveProperty('description');
+ });
+ });
+
+ describe('nested fields', () => {
+ test("should automatically wrap nested fields in KQL's nested syntax", async () => {
+ const prefix = 'ch';
+ const suffix = '';
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ prefix, suffix })
+ );
+
+ const suggestion = suggestions.find(({ field }) => field.name === 'nestedField.child');
+
+ expect(suggestion).toBeDefined();
+
+ if (suggestion) {
+ expect(suggestion.text).toBe('nestedField:{ child }');
+
+ // For most suggestions the cursor can be placed at the end of the suggestion text, but
+ // for the nested field syntax we want to place the cursor inside the curly braces
+ expect(suggestion.cursorIndex).toBe(20);
+ }
+ });
+
+ test('should narrow suggestions to children of a nested path if provided', async () => {
+ const prefix = 'ch';
+ const suffix = '';
+
+ const allSuggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ prefix, suffix })
+ );
+ expect(allSuggestions.length).toBeGreaterThan(2);
+
+ const nestedSuggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ prefix,
+ suffix,
+ nestedPath: 'nestedField',
+ })
+ );
+ expect(nestedSuggestions).toHaveLength(2);
+ });
+
+ test("should not wrap the suggestion in KQL's nested syntax if the correct nested path is already provided", async () => {
+ const prefix = 'ch';
+ const suffix = '';
+
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ prefix,
+ suffix,
+ nestedPath: 'nestedField',
+ })
+ );
+ const suggestion = suggestions.find(({ field }) => field.name === 'nestedField.child');
+
+ expect(suggestion).toBeDefined();
+
+ if (suggestion) {
+ expect(suggestion.text).toBe('child ');
+ }
+ });
+
+ test('should handle fields nested multiple levels deep', async () => {
+ const prefix = 'doubly';
+ const suffix = '';
+
+ const suggestionsWithNoPath = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ prefix, suffix })
+ );
+ expect(suggestionsWithNoPath).toHaveLength(1);
+ const [noPathSuggestion] = suggestionsWithNoPath;
+ expect(noPathSuggestion.text).toBe('nestedField.nestedChild:{ doublyNestedChild }');
+
+ const suggestionsWithPartialPath = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ prefix,
+ suffix,
+ nestedPath: 'nestedField',
+ })
+ );
+ expect(suggestionsWithPartialPath).toHaveLength(1);
+ const [partialPathSuggestion] = suggestionsWithPartialPath;
+ expect(partialPathSuggestion.text).toBe('nestedChild:{ doublyNestedChild }');
+
+ const suggestionsWithFullPath = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ prefix,
+ suffix,
+ nestedPath: 'nestedField.nestedChild',
+ })
+ );
+ expect(suggestionsWithFullPath).toHaveLength(1);
+ const [fullPathSuggestion] = suggestionsWithFullPath;
+ expect(fullPathSuggestion.text).toBe('doublyNestedChild ');
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.tsx
similarity index 66%
rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.js
rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.tsx
index 3e5c92dfc007f..a8af884c24fc3 100644
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.js
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.tsx
@@ -5,32 +5,42 @@
*/
import React from 'react';
import { flatten } from 'lodash';
-import { escapeKuery } from './escape_kuery';
-import { sortPrefixFirst } from './sort_prefix_first';
-import { isFilterable } from '../../../../../../src/plugins/data/public';
import { FormattedMessage } from '@kbn/i18n/react';
+import { escapeKuery } from './lib/escape_kuery';
+import { sortPrefixFirst } from './sort_prefix_first';
+import { IFieldType, isFilterable, autocomplete } from '../../../../../../src/plugins/data/public';
+import { KqlQuerySuggestionProvider } from './types';
-const type = 'field';
-
-function getDescription(fieldName) {
+const getDescription = (field: IFieldType) => {
return (
{fieldName} }}
+ values={{ fieldName: {field.name} }}
/>
);
-}
+};
-export function getSuggestionsProvider({ indexPatterns }) {
- const allFields = flatten(
- indexPatterns.map(indexPattern => {
- return indexPattern.fields.filter(isFilterable);
- })
- );
- return function getFieldSuggestions({ start, end, prefix, suffix, nestedPath = '' }) {
+const keywordComparator = (first: IFieldType, second: IFieldType) => {
+ const extensions = ['raw', 'keyword'];
+ if (extensions.map(ext => `${first.name}.${ext}`).includes(second.name)) {
+ return 1;
+ } else if (extensions.map(ext => `${second.name}.${ext}`).includes(first.name)) {
+ return -1;
+ }
+
+ return first.name.localeCompare(second.name);
+};
+
+export const setupGetFieldSuggestions: KqlQuerySuggestionProvider = core => {
+ return ({ indexPatterns }, { start, end, prefix, suffix, nestedPath = '' }) => {
+ const allFields = flatten(
+ indexPatterns.map(indexPattern => {
+ return indexPattern.fields.filter(isFilterable);
+ })
+ );
const search = `${prefix}${suffix}`.trim().toLowerCase();
const matchingFields = allFields.filter(field => {
return (
@@ -44,7 +54,8 @@ export function getSuggestionsProvider({ indexPatterns }) {
);
});
const sortedFields = sortPrefixFirst(matchingFields.sort(keywordComparator), search, 'name');
- const suggestions = sortedFields.map(field => {
+
+ const suggestions: autocomplete.FieldQuerySuggestion[] = sortedFields.map(field => {
const remainingPath =
field.subType && field.subType.nested
? field.subType.nested.path.slice(nestedPath ? nestedPath.length + 1 : 0)
@@ -55,23 +66,23 @@ export function getSuggestionsProvider({ indexPatterns }) {
field.name.slice(field.subType.nested.path.length + 1)
)} }`
: `${escapeKuery(field.name.slice(nestedPath ? nestedPath.length + 1 : 0))} `;
- const description = getDescription(field.name);
+ const description = getDescription(field);
const cursorIndex =
field.subType && field.subType.nested && remainingPath.length > 0
? text.length - 2
: text.length;
- return { type, text, description, start, end, cursorIndex, field };
+
+ return {
+ type: autocomplete.QuerySuggestionsTypes.Field,
+ text,
+ description,
+ start,
+ end,
+ cursorIndex,
+ field,
+ };
});
- return suggestions;
- };
-}
-function keywordComparator(first, second) {
- const extensions = ['raw', 'keyword'];
- if (extensions.map(ext => `${first.name}.${ext}`).includes(second.name)) {
- return 1;
- } else if (extensions.map(ext => `${second.name}.${ext}`).includes(first.name)) {
- return -1;
- }
- return first.name.localeCompare(second.name);
-}
+ return Promise.resolve(suggestions);
+ };
+};
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js
deleted file mode 100644
index b877f9eb852d5..0000000000000
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js
+++ /dev/null
@@ -1,58 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { flatten, uniq } from 'lodash';
-import { getSuggestionsProvider as field } from './field';
-import { getSuggestionsProvider as value } from './value';
-import { getSuggestionsProvider as operator } from './operator';
-import { getSuggestionsProvider as conjunction } from './conjunction';
-import { esKuery } from '../../../../../../src/plugins/data/public';
-
-const cursorSymbol = '@kuery-cursor@';
-const providers = {
- field,
- value,
- operator,
- conjunction,
-};
-
-function dedup(suggestions) {
- return uniq(suggestions, ({ type, text, start, end }) => [type, text, start, end].join('|'));
-}
-
-const getProviderByType = (type, args) => providers[type](args);
-
-export const setupKqlQuerySuggestionProvider = ({ uiSettings }) => ({
- indexPatterns,
- boolFilter,
- query,
- selectionStart,
- selectionEnd,
- signal,
-}) => {
- const cursoredQuery = `${query.substr(0, selectionStart)}${cursorSymbol}${query.substr(
- selectionEnd
- )}`;
-
- let cursorNode;
- try {
- cursorNode = esKuery.fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true });
- } catch (e) {
- cursorNode = {};
- }
-
- const { suggestionTypes = [] } = cursorNode;
- const suggestionsByType = suggestionTypes.map(type =>
- getProviderByType(type, {
- config: uiSettings,
- indexPatterns,
- boolFilter,
- })(cursorNode, signal)
- );
- return Promise.all(suggestionsByType).then(suggestionsByType =>
- dedup(flatten(suggestionsByType))
- );
-};
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.ts
new file mode 100644
index 0000000000000..2cc15fe4c9280
--- /dev/null
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CoreSetup } from 'kibana/public';
+import { $Keys } from 'utility-types';
+import { flatten, uniq } from 'lodash';
+import { setupGetFieldSuggestions } from './field';
+import { setupGetValueSuggestions } from './value';
+import { setupGetOperatorSuggestions } from './operator';
+import { setupGetConjunctionSuggestions } from './conjunction';
+import { esKuery, autocomplete } from '../../../../../../src/plugins/data/public';
+
+const cursorSymbol = '@kuery-cursor@';
+
+const dedup = (suggestions: autocomplete.QuerySuggestion[]): autocomplete.QuerySuggestion[] =>
+ uniq(suggestions, ({ type, text, start, end }) => [type, text, start, end].join('|'));
+
+export const setupKqlQuerySuggestionProvider = (
+ core: CoreSetup
+): autocomplete.QuerySuggestionsGetFn => {
+ const providers = {
+ field: setupGetFieldSuggestions(core),
+ value: setupGetValueSuggestions(core),
+ operator: setupGetOperatorSuggestions(core),
+ conjunction: setupGetConjunctionSuggestions(core),
+ };
+
+ const getSuggestionsByType = (
+ cursoredQuery: string,
+ querySuggestionsArgs: autocomplete.QuerySuggestionsGetFnArgs
+ ): Array> | [] => {
+ try {
+ const cursorNode = esKuery.fromKueryExpression(cursoredQuery, {
+ cursorSymbol,
+ parseCursor: true,
+ });
+
+ return cursorNode.suggestionTypes.map((type: $Keys) =>
+ providers[type](querySuggestionsArgs, cursorNode)
+ );
+ } catch (e) {
+ return [];
+ }
+ };
+
+ return querySuggestionsArgs => {
+ const { query, selectionStart, selectionEnd } = querySuggestionsArgs;
+ const cursoredQuery = `${query.substr(0, selectionStart)}${cursorSymbol}${query.substr(
+ selectionEnd
+ )}`;
+
+ return Promise.all(
+ getSuggestionsByType(cursoredQuery, querySuggestionsArgs)
+ ).then(suggestionsByType => dedup(flatten(suggestionsByType)));
+ };
+};
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/escape_kuery.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.test.ts
similarity index 55%
rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/escape_kuery.js
rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.test.ts
index 2127194c9a890..a4a1d977a207f 100644
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/escape_kuery.js
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.test.ts
@@ -4,55 +4,62 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import expect from '@kbn/expect';
-import { escapeQuotes, escapeKuery } from '../escape_kuery';
+import { escapeQuotes, escapeKuery } from './escape_kuery';
-describe('Kuery escape', function() {
- it('should escape quotes', function() {
+describe('Kuery escape', () => {
+ test('should escape quotes', () => {
const value = 'I said, "Hello."';
const expected = 'I said, \\"Hello.\\"';
- expect(escapeQuotes(value)).to.be(expected);
+
+ expect(escapeQuotes(value)).toBe(expected);
});
- it('should escape special characters', function() {
+ test('should escape special characters', () => {
const value = `This \\ has (a lot of) characters, don't you *think*? "Yes."`;
const expected = `This \\\\ has \\(a lot of\\) \\ characters, don't you \\*think\\*? \\"Yes.\\"`;
- expect(escapeKuery(value)).to.be(expected);
+
+ expect(escapeKuery(value)).toBe(expected);
});
- it('should escape keywords', function() {
+ test('should escape keywords', () => {
const value = 'foo and bar or baz not qux';
const expected = 'foo \\and bar \\or baz \\not qux';
- expect(escapeKuery(value)).to.be(expected);
+
+ expect(escapeKuery(value)).toBe(expected);
});
- it('should escape keywords next to each other', function() {
+ test('should escape keywords next to each other', () => {
const value = 'foo and bar or not baz';
const expected = 'foo \\and bar \\or \\not baz';
- expect(escapeKuery(value)).to.be(expected);
+
+ expect(escapeKuery(value)).toBe(expected);
});
- it('should not escape keywords without surrounding spaces', function() {
+ test('should not escape keywords without surrounding spaces', () => {
const value = 'And this has keywords, or does it not?';
const expected = 'And this has keywords, \\or does it not?';
- expect(escapeKuery(value)).to.be(expected);
+
+ expect(escapeKuery(value)).toBe(expected);
});
- it('should escape uppercase keywords', function() {
+ test('should escape uppercase keywords', () => {
const value = 'foo AND bar';
const expected = 'foo \\AND bar';
- expect(escapeKuery(value)).to.be(expected);
+
+ expect(escapeKuery(value)).toBe(expected);
});
- it('should escape both keywords and special characters', function() {
+ test('should escape both keywords and special characters', () => {
const value = 'Hello, world, and to meet you!';
const expected = 'Hello, world, \\and \\ to meet you!';
- expect(escapeKuery(value)).to.be(expected);
+
+ expect(escapeKuery(value)).toBe(expected);
});
- it('should escape newlines and tabs', () => {
+ test('should escape newlines and tabs', () => {
const value = 'This\nhas\tnewlines\r\nwith\ttabs';
const expected = 'This\\nhas\\tnewlines\\r\\nwith\\ttabs';
- expect(escapeKuery(value)).to.be(expected);
+
+ expect(escapeKuery(value)).toBe(expected);
});
});
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/escape_kuery.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.ts
similarity index 57%
rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/escape_kuery.js
rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.ts
index 5d9bfe6143c22..a00082f8c7d7c 100644
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/escape_kuery.js
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.ts
@@ -6,29 +6,29 @@
import { flow } from 'lodash';
-export function escapeQuotes(string) {
- return string.replace(/"/g, '\\"');
+export function escapeQuotes(str: string) {
+ return str.replace(/"/g, '\\"');
}
export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace);
// See the SpecialCharacter rule in kuery.peg
-function escapeSpecialCharacters(string) {
- return string.replace(/[\\():<>"*]/g, '\\$&'); // $& means the whole matched string
+function escapeSpecialCharacters(str: string) {
+ return str.replace(/[\\():<>"*]/g, '\\$&'); // $& means the whole matched string
}
// See the Keyword rule in kuery.peg
-function escapeAndOr(string) {
- return string.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3');
+function escapeAndOr(str: string) {
+ return str.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3');
}
-function escapeNot(string) {
- return string.replace(/not(\s+)/gi, '\\$&');
+function escapeNot(str: string) {
+ return str.replace(/not(\s+)/gi, '\\$&');
}
// See the Space rule in kuery.peg
-function escapeWhitespace(string) {
- return string
+function escapeWhitespace(str: string) {
+ return str
.replace(/\t/g, '\\t')
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n');
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.test.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.test.ts
new file mode 100644
index 0000000000000..acafc4e169c8f
--- /dev/null
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.test.ts
@@ -0,0 +1,96 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import indexPatternResponse from './__fixtures__/index_pattern_response.json';
+
+import { setupGetOperatorSuggestions } from './operator';
+import { autocomplete, esKuery } from '../../../../../../src/plugins/data/public';
+import { coreMock } from '../../../../../../src/core/public/mocks';
+
+const mockKueryNode = (kueryNode: Partial) =>
+ (kueryNode as unknown) as esKuery.KueryNode;
+
+describe('Kuery operator suggestions', () => {
+ let getSuggestions: ReturnType;
+ let querySuggestionsArgs: autocomplete.QuerySuggestionsGetFnArgs;
+
+ beforeEach(() => {
+ querySuggestionsArgs = ({
+ indexPatterns: [indexPatternResponse],
+ } as unknown) as autocomplete.QuerySuggestionsGetFnArgs;
+
+ getSuggestions = setupGetOperatorSuggestions(coreMock.createSetup());
+ });
+
+ test('should return a function', () => {
+ expect(typeof getSuggestions).toBe('function');
+ });
+
+ test('should not return suggestions for non-fields', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ fieldName: 'foo' })
+ );
+
+ expect(suggestions).toEqual([]);
+ });
+
+ test('should return exists for every field', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ fieldName: 'custom_user_field',
+ })
+ );
+
+ expect(suggestions.length).toEqual(1);
+ expect(suggestions[0].text).toBe(': * ');
+ });
+
+ test('should return equals for string fields', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ fieldName: 'machine.os' })
+ );
+
+ expect(suggestions.find(({ text }) => text === ': ')).toBeDefined();
+ expect(suggestions.find(({ text }) => text === '< ')).not.toBeDefined();
+ });
+
+ test('should return numeric operators for numeric fields', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ fieldName: 'bytes' })
+ );
+
+ expect(suggestions.find(({ text }) => text === ': ')).toBeDefined();
+ expect(suggestions.find(({ text }) => text === '< ')).toBeDefined();
+ });
+
+ test('should have descriptions', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ fieldName: 'bytes' })
+ );
+
+ expect(suggestions.length).toBeGreaterThan(0);
+
+ suggestions.forEach(suggestion => {
+ expect(suggestion).toHaveProperty('description');
+ });
+ });
+
+ test('should handle nested paths', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ fieldName: 'child',
+ nestedPath: 'nestedField',
+ })
+ );
+
+ expect(suggestions.length).toBeGreaterThan(0);
+ });
+});
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.tsx
similarity index 82%
rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.js
rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.tsx
index 173a24b3f5f1e..6e9010c4310fb 100644
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.js
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.tsx
@@ -6,8 +6,11 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
+import { $Keys } from 'utility-types';
import { flatten } from 'lodash';
-const type = 'operator';
+
+import { KqlQuerySuggestionProvider } from './types';
+import { autocomplete } from '../../../../../../src/plugins/data/public';
const equalsText = (
),
+ fieldTypes: undefined,
},
};
-function getDescription(operator) {
- const { description } = operators[operator];
- return {description}
;
-}
+type Operators = $Keys;
+
+const getOperatorByName = (operator: string) => operators[operator as Operators];
+const getDescription = (operator: string) => {getOperatorByName(operator).description}
;
-export function getSuggestionsProvider({ indexPatterns }) {
- const allFields = flatten(
- indexPatterns.map(indexPattern => {
- return indexPattern.fields.slice();
- })
- );
- return function getOperatorSuggestions({ end, fieldName, nestedPath }) {
+export const setupGetOperatorSuggestions: KqlQuerySuggestionProvider = () => {
+ return ({ indexPatterns }, { end, fieldName, nestedPath }) => {
+ const allFields = flatten(
+ indexPatterns.map(indexPattern => {
+ return indexPattern.fields.slice();
+ })
+ );
const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName;
- const fields = allFields.filter(field => field.name === fullFieldName);
- return flatten(
- fields.map(field => {
+ const fields = allFields
+ .filter(field => field.name === fullFieldName)
+ .map(field => {
const matchingOperators = Object.keys(operators).filter(operator => {
- const { fieldTypes } = operators[operator];
+ const { fieldTypes } = getOperatorByName(operator);
+
return !fieldTypes || fieldTypes.includes(field.type);
});
- const suggestions = matchingOperators.map(operator => {
- const text = operator + ' ';
- const description = getDescription(operator);
- return { type, text, description, start: end, end };
- });
+
+ const suggestions = matchingOperators.map(operator => ({
+ type: autocomplete.QuerySuggestionsTypes.Operator,
+ text: operator + ' ',
+ description: getDescription(operator),
+ start: end,
+ end,
+ }));
return suggestions;
- })
- );
+ });
+
+ return Promise.resolve(flatten(fields));
};
-}
+};
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts
index 123e440b75231..03e1a9099f1ab 100644
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts
@@ -14,7 +14,9 @@ export function sortPrefixFirst(array: any[], prefix?: string | number, property
const partitions = partition(array, entry => {
const value = ('' + (property ? entry[property] : entry)).toLowerCase();
+
return value.startsWith(lowerCasePrefix);
});
+
return [...partitions[0], ...partitions[1]];
}
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/types.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/types.ts
new file mode 100644
index 0000000000000..c51b75e001b9f
--- /dev/null
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/types.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CoreSetup } from 'kibana/public';
+import { esKuery, autocomplete } from '../../../../../../src/plugins/data/public';
+
+export type KqlQuerySuggestionProvider = (
+ core: CoreSetup
+) => (
+ querySuggestionsGetFnArgs: autocomplete.QuerySuggestionsGetFnArgs,
+ kueryNode: esKuery.KueryNode
+) => Promise;
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js
deleted file mode 100644
index 9d0d70fd95747..0000000000000
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js
+++ /dev/null
@@ -1,58 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { flatten } from 'lodash';
-import { escapeQuotes } from './escape_kuery';
-import { npStart } from 'ui/new_platform';
-
-const type = 'value';
-
-export function getSuggestionsProvider({ indexPatterns, boolFilter }) {
- const allFields = flatten(
- indexPatterns.map(indexPattern => {
- return indexPattern.fields.map(field => ({
- ...field,
- indexPattern,
- }));
- })
- );
-
- return function getValueSuggestions(
- { start, end, prefix, suffix, fieldName, nestedPath },
- signal
- ) {
- const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName;
- const fields = allFields.filter(field => field.name === fullFieldName);
- const query = `${prefix}${suffix}`.trim();
- const { getValueSuggestions } = npStart.plugins.data.autocomplete;
-
- const suggestionsByField = fields.map(field =>
- getValueSuggestions({
- indexPattern: field.indexPattern,
- field,
- query,
- boolFilter,
- signal,
- }).then(data => {
- const quotedValues = data.map(value =>
- typeof value === 'string' ? `"${escapeQuotes(value)}"` : `${value}`
- );
- return wrapAsSuggestions(start, end, query, quotedValues);
- })
- );
-
- return Promise.all(suggestionsByField).then(suggestions => flatten(suggestions));
- };
-}
-
-function wrapAsSuggestions(start, end, query, values) {
- return values
- .filter(value => value.toLowerCase().includes(query.toLowerCase()))
- .map(value => {
- const text = `${value} `;
- return { type, text, start, end };
- });
-}
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js
deleted file mode 100644
index f5b652d2e2164..0000000000000
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js
+++ /dev/null
@@ -1,146 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { getSuggestionsProvider } from './value';
-import indexPatternResponse from './__fixtures__/index_pattern_response.json';
-import { npStart } from 'ui/new_platform';
-
-jest.mock('ui/new_platform', () => ({
- npStart: {
- plugins: {
- data: {
- autocomplete: {
- getValueSuggestions: jest.fn(({ field }) => {
- let res;
- if (field.type === 'boolean') {
- res = [true, false];
- } else if (field.name === 'machine.os') {
- res = ['Windo"ws', "Mac'", 'Linux'];
- } else if (field.name === 'nestedField.child') {
- res = ['foo'];
- } else {
- res = [];
- }
- return Promise.resolve(res);
- }),
- },
- },
- },
- },
-}));
-
-describe('Kuery value suggestions', function() {
- let indexPatterns;
- let getSuggestions;
-
- beforeEach(() => {
- indexPatterns = [indexPatternResponse];
- getSuggestions = getSuggestionsProvider({ indexPatterns });
- jest.clearAllMocks();
- });
-
- test('should return a function', function() {
- expect(typeof getSuggestions).toBe('function');
- });
-
- test('should not search for non existing field', async () => {
- const fieldName = 'i_dont_exist';
- const prefix = '';
- const suffix = '';
-
- const suggestions = await getSuggestions({ fieldName, prefix, suffix });
- expect(suggestions.map(({ text }) => text)).toEqual([]);
-
- expect(npStart.plugins.data.autocomplete.getValueSuggestions).toHaveBeenCalledTimes(0);
- });
-
- test('should format suggestions', async () => {
- const start = 1;
- const end = 5;
- const suggestions = await getSuggestions({
- fieldName: 'ssl',
- prefix: '',
- suffix: '',
- start,
- end,
- });
-
- expect(suggestions[0].type).toEqual('value');
- expect(suggestions[0].start).toEqual(start);
- expect(suggestions[0].end).toEqual(end);
- });
-
- test('should handle nested paths', async () => {
- const suggestions = await getSuggestions({
- fieldName: 'child',
- nestedPath: 'nestedField',
- prefix: '',
- suffix: '',
- });
- expect(suggestions.length).toEqual(1);
- expect(suggestions[0].text).toEqual('"foo" ');
- });
-
- describe('Boolean suggestions', function() {
- test('should stringify boolean fields', async () => {
- const suggestions = await getSuggestions({ fieldName: 'ssl', prefix: '', suffix: '' });
-
- expect(suggestions.map(({ text }) => text)).toEqual(['true ', 'false ']);
- expect(npStart.plugins.data.autocomplete.getValueSuggestions).toHaveBeenCalledTimes(1);
- });
-
- test('should filter out boolean suggestions', async () => {
- const suggestions = await getSuggestions({ fieldName: 'ssl', prefix: 'fa', suffix: '' });
-
- expect(suggestions.length).toEqual(1);
- });
- });
-
- describe('String suggestions', function() {
- test('should merge prefix and suffix', async () => {
- const prefix = 'he';
- const suffix = 'llo';
-
- await getSuggestions({ fieldName: 'machine.os.raw', prefix, suffix });
-
- expect(npStart.plugins.data.autocomplete.getValueSuggestions).toHaveBeenCalledTimes(1);
- expect(npStart.plugins.data.autocomplete.getValueSuggestions).toBeCalledWith(
- expect.objectContaining({
- field: expect.any(Object),
- query: prefix + suffix,
- })
- );
- });
-
- test('should escape quotes in suggestions', async () => {
- const suggestions = await getSuggestions({ fieldName: 'machine.os', prefix: '', suffix: '' });
-
- expect(suggestions[0].text).toEqual('"Windo\\"ws" ');
- expect(suggestions[1].text).toEqual('"Mac\'" ');
- expect(suggestions[2].text).toEqual('"Linux" ');
- });
-
- test('should filter out string suggestions', async () => {
- const suggestions = await getSuggestions({
- fieldName: 'machine.os',
- prefix: 'banana',
- suffix: '',
- });
-
- expect(suggestions.length).toEqual(0);
- });
-
- test('should partially filter out string suggestions - case insensitive', async () => {
- const suggestions = await getSuggestions({
- fieldName: 'machine.os',
- prefix: 'ma',
- suffix: '',
- });
-
- expect(suggestions.length).toEqual(1);
- });
- });
-});
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.ts
new file mode 100644
index 0000000000000..5ffe30c877868
--- /dev/null
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.ts
@@ -0,0 +1,192 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { setupGetValueSuggestions } from './value';
+import indexPatternResponse from './__fixtures__/index_pattern_response.json';
+import { coreMock } from '../../../../../../src/core/public/mocks';
+import { autocomplete, esKuery } from '../../../../../../src/plugins/data/public';
+import { setAutocompleteService } from '../services';
+
+const mockKueryNode = (kueryNode: Partial) =>
+ (kueryNode as unknown) as esKuery.KueryNode;
+
+describe('Kuery value suggestions', () => {
+ let getSuggestions: ReturnType;
+ let querySuggestionsArgs: autocomplete.QuerySuggestionsGetFnArgs;
+ let autocompleteServiceMock: any;
+
+ beforeEach(() => {
+ getSuggestions = setupGetValueSuggestions(coreMock.createSetup());
+ querySuggestionsArgs = ({
+ indexPatterns: [indexPatternResponse],
+ } as unknown) as autocomplete.QuerySuggestionsGetFnArgs;
+
+ autocompleteServiceMock = {
+ getValueSuggestions: jest.fn(({ field }) => {
+ let res: any[];
+
+ if (field.type === 'boolean') {
+ res = [true, false];
+ } else if (field.name === 'machine.os') {
+ res = ['Windo"ws', "Mac'", 'Linux'];
+ } else if (field.name === 'nestedField.child') {
+ res = ['foo'];
+ } else {
+ res = [];
+ }
+ return Promise.resolve(res);
+ }),
+ };
+ setAutocompleteService(autocompleteServiceMock);
+
+ jest.clearAllMocks();
+ });
+
+ test('should return a function', () => {
+ expect(typeof getSuggestions).toBe('function');
+ });
+
+ test('should not search for non existing field', async () => {
+ const fieldName = 'i_dont_exist';
+ const prefix = '';
+ const suffix = '';
+
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({ fieldName, prefix, suffix })
+ );
+
+ expect(suggestions.map(({ text }) => text)).toEqual([]);
+ expect(autocompleteServiceMock.getValueSuggestions).toHaveBeenCalledTimes(0);
+ });
+
+ test('should format suggestions', async () => {
+ const start = 1;
+ const end = 5;
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ fieldName: 'ssl',
+ prefix: '',
+ suffix: '',
+ start,
+ end,
+ })
+ );
+
+ expect(suggestions[0].type).toEqual('value');
+ expect(suggestions[0].start).toEqual(start);
+ expect(suggestions[0].end).toEqual(end);
+ });
+
+ test('should handle nested paths', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ fieldName: 'child',
+ nestedPath: 'nestedField',
+ prefix: '',
+ suffix: '',
+ })
+ );
+
+ expect(suggestions.length).toEqual(1);
+ expect(suggestions[0].text).toEqual('"foo" ');
+ });
+
+ describe('Boolean suggestions', () => {
+ test('should stringify boolean fields', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ fieldName: 'ssl',
+ prefix: '',
+ suffix: '',
+ })
+ );
+
+ expect(suggestions.map(({ text }) => text)).toEqual(['true ', 'false ']);
+ expect(autocompleteServiceMock.getValueSuggestions).toHaveBeenCalledTimes(1);
+ });
+
+ test('should filter out boolean suggestions', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ fieldName: 'ssl',
+ prefix: 'fa',
+ suffix: '',
+ })
+ );
+
+ expect(suggestions.length).toEqual(1);
+ });
+ });
+
+ describe('String suggestions', () => {
+ test('should merge prefix and suffix', async () => {
+ const prefix = 'he';
+ const suffix = 'llo';
+
+ await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ fieldName: 'machine.os.raw',
+ prefix,
+ suffix,
+ })
+ );
+
+ expect(autocompleteServiceMock.getValueSuggestions).toHaveBeenCalledTimes(1);
+ expect(autocompleteServiceMock.getValueSuggestions).toBeCalledWith(
+ expect.objectContaining({
+ field: expect.any(Object),
+ query: prefix + suffix,
+ })
+ );
+ });
+
+ test('should escape quotes in suggestions', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ fieldName: 'machine.os',
+ prefix: '',
+ suffix: '',
+ })
+ );
+
+ expect(suggestions[0].text).toEqual('"Windo\\"ws" ');
+ expect(suggestions[1].text).toEqual('"Mac\'" ');
+ expect(suggestions[2].text).toEqual('"Linux" ');
+ });
+
+ test('should filter out string suggestions', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ fieldName: 'machine.os',
+ prefix: 'banana',
+ suffix: '',
+ })
+ );
+
+ expect(suggestions.length).toEqual(0);
+ });
+
+ test('should partially filter out string suggestions - case insensitive', async () => {
+ const suggestions = await getSuggestions(
+ querySuggestionsArgs,
+ mockKueryNode({
+ fieldName: 'machine.os',
+ prefix: 'ma',
+ suffix: '',
+ })
+ );
+
+ expect(suggestions.length).toEqual(1);
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.ts
new file mode 100644
index 0000000000000..242b9ccba3508
--- /dev/null
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { flatten } from 'lodash';
+import { escapeQuotes } from './lib/escape_kuery';
+import { KqlQuerySuggestionProvider } from './types';
+import { getAutocompleteService } from '../services';
+import { autocomplete } from '../../../../../../src/plugins/data/public';
+
+const wrapAsSuggestions = (start: number, end: number, query: string, values: string[]) =>
+ values
+ .filter(value => value.toLowerCase().includes(query.toLowerCase()))
+ .map(value => ({
+ type: autocomplete.QuerySuggestionsTypes.Value,
+ text: `${value} `,
+ start,
+ end,
+ }));
+
+export const setupGetValueSuggestions: KqlQuerySuggestionProvider = core => {
+ return async (
+ { indexPatterns, boolFilter, signal },
+ { start, end, prefix, suffix, fieldName, nestedPath }
+ ): Promise => {
+ const allFields = flatten(
+ indexPatterns.map(indexPattern =>
+ indexPattern.fields.map(field => ({
+ ...field,
+ indexPattern,
+ }))
+ )
+ );
+
+ const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName;
+ const fields = allFields.filter(field => field.name === fullFieldName);
+ const query = `${prefix}${suffix}`.trim();
+ const { getValueSuggestions } = getAutocompleteService();
+
+ const data = await Promise.all(
+ fields.map(field =>
+ getValueSuggestions({
+ indexPattern: field.indexPattern,
+ field,
+ query,
+ boolFilter,
+ signal,
+ }).then(valueSuggestions => {
+ const quotedValues = valueSuggestions.map(value =>
+ typeof value === 'string' ? `"${escapeQuotes(value)}"` : `${value}`
+ );
+
+ return wrapAsSuggestions(start, end, query, quotedValues);
+ })
+ )
+ );
+
+ return flatten(data);
+ };
+};
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/legacy.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/legacy.ts
index 52abe21c055ee..303fe8c557fbd 100644
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/legacy.ts
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/legacy.ts
@@ -8,13 +8,20 @@ import { PluginInitializerContext } from 'src/core/public';
import { npSetup, npStart } from 'ui/new_platform';
import { plugin } from './index';
-import { KueryAutocompletePluginSetupDependencies } from './plugin';
+import {
+ KueryAutocompletePluginSetupDependencies,
+ KueryAutocompletePluginStartDependencies,
+} from './plugin';
-const plugins: Readonly = {
+const pluginsSetup: Readonly = {
data: npSetup.plugins.data,
};
+const pluginsStart: Readonly = {
+ data: npStart.plugins.data,
+};
+
const pluginInstance = plugin({} as PluginInitializerContext);
-export const setup = pluginInstance.setup(npSetup.core, plugins);
-export const start = pluginInstance.start(npStart.core);
+export const setup = pluginInstance.setup(npSetup.core, pluginsSetup);
+export const start = pluginInstance.start(npStart.core, pluginsStart);
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts
index 216e0f49ccd34..81737c4636532 100644
--- a/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts
@@ -6,8 +6,7 @@
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { Plugin as DataPublicPlugin } from '../../../../../src/plugins/data/public';
-
-// @ts-ignore
+import { setAutocompleteService } from './services';
import { setupKqlQuerySuggestionProvider } from './kql_query_suggestion';
/** @internal */
@@ -15,6 +14,11 @@ export interface KueryAutocompletePluginSetupDependencies {
data: ReturnType;
}
+/** @internal */
+export interface KueryAutocompletePluginStartDependencies {
+ data: ReturnType;
+}
+
const KUERY_LANGUAGE_NAME = 'kuery';
/** @internal */
@@ -26,12 +30,12 @@ export class KueryAutocompletePlugin implements Plugin, void> {
}
public async setup(core: CoreSetup, plugins: KueryAutocompletePluginSetupDependencies) {
- const kueryProvider = setupKqlQuerySuggestionProvider(core, plugins);
+ const kueryProvider = setupKqlQuerySuggestionProvider(core);
plugins.data.autocomplete.addQuerySuggestionProvider(KUERY_LANGUAGE_NAME, kueryProvider);
}
- public start(core: CoreStart) {
- // nothing to do here yet
+ public start(core: CoreStart, plugins: KueryAutocompletePluginStartDependencies) {
+ setAutocompleteService(plugins.data.autocomplete);
}
}
diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/services.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/services.ts
new file mode 100644
index 0000000000000..1ec48e597f636
--- /dev/null
+++ b/x-pack/legacy/plugins/kuery_autocomplete/public/services.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { createGetterSetter } from '../../../../../src/plugins/kibana_utils/public';
+import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
+
+export const [getAutocompleteService, setAutocompleteService] = createGetterSetter<
+ DataPublicPluginStart['autocomplete']
+>('Autocomplete');
diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx
deleted file mode 100644
index 29fe690f4a43b..0000000000000
--- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx
+++ /dev/null
@@ -1,31 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { FC } from 'react';
-
-import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
-
-interface Props {
- example: string | object;
-}
-
-export const Example: FC = ({ example }) => {
- const exampleStr = typeof example === 'string' ? example : JSON.stringify(example);
-
- // Use 95% width for each example so that the truncation ellipses show up when
- // wrapped inside a tooltip.
- return (
-
-
-
-
- {exampleStr}
-
-
-
-
- );
-};
diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx
index 0bf911c1edf86..c8eb810115401 100644
--- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx
+++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx
@@ -6,12 +6,10 @@
import React, { FC } from 'react';
-import { EuiSpacer, EuiText } from '@elastic/eui';
+import { EuiListGroup, EuiListGroupItem, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { Example } from './example';
-
interface Props {
examples: Array;
}
@@ -22,7 +20,14 @@ export const ExamplesList: FC = ({ examples }) => {
}
const examplesContent = examples.map((example, i) => {
- return ;
+ return (
+
+ );
});
return (
@@ -39,7 +44,9 @@ export const ExamplesList: FC = ({ examples }) => {
- {examplesContent}
+
+ {examplesContent}
+
);
};
diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts
index e602d5fc608d3..1746345879192 100644
--- a/x-pack/legacy/plugins/reporting/common/constants.ts
+++ b/x-pack/legacy/plugins/reporting/common/constants.ts
@@ -14,8 +14,11 @@ export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY =
export const API_BASE_URL = '/api/reporting'; // "Generation URL" from share menu
export const API_BASE_URL_V1 = '/api/reporting/v1'; //
export const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`;
+export const API_LIST_URL = '/api/reporting/jobs';
+export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`;
export const CONTENT_TYPE_CSV = 'text/csv';
+export const CSV_REPORTING_ACTION = 'downloadCsvReport';
export const WHITELISTED_JOB_CONTENT_TYPES = [
'application/json',
diff --git a/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx
index ac966ceb99736..aaf4021302a97 100644
--- a/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx
+++ b/x-pack/legacy/plugins/reporting/public/components/reporting_panel_content.tsx
@@ -7,11 +7,10 @@
import { EuiButton, EuiCopy, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React, { Component, ReactElement } from 'react';
-import { KFetchError } from 'ui/kfetch/kfetch_error';
import { toastNotifications } from 'ui/notify';
import url from 'url';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
-import { reportingClient } from '../lib/reporting_client';
+import * as reportingClient from '../lib/reporting_client';
interface Props {
reportType: string;
@@ -217,8 +216,8 @@ class ReportingPanelContentUi extends Component {
});
this.props.onClose();
})
- .catch((kfetchError: KFetchError) => {
- if (kfetchError.message === 'not exportable') {
+ .catch((error: any) => {
+ if (error.message === 'not exportable') {
return toastNotifications.addWarning({
title: intl.formatMessage(
{
@@ -237,7 +236,7 @@ class ReportingPanelContentUi extends Component {
}
const defaultMessage =
- kfetchError.res.status === 403 ? (
+ error?.res?.status === 403 ? (
{
id: 'xpack.reporting.panelContent.notification.reportingErrorTitle',
defaultMessage: 'Reporting error',
}),
- text: toMountPoint(kfetchError.message || defaultMessage),
+ text: toMountPoint(error.message || defaultMessage),
'data-test-subj': 'queueReportError',
});
});
diff --git a/x-pack/legacy/plugins/reporting/public/lib/download_report.ts b/x-pack/legacy/plugins/reporting/public/lib/download_report.ts
index 18bae64f8788d..54194c87afabc 100644
--- a/x-pack/legacy/plugins/reporting/public/lib/download_report.ts
+++ b/x-pack/legacy/plugins/reporting/public/lib/download_report.ts
@@ -4,11 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import chrome from 'ui/chrome';
+import { npStart } from 'ui/new_platform';
import { API_BASE_URL } from '../../common/constants';
+const { core } = npStart;
+
export function getReportURL(jobId: string) {
- const apiBaseUrl = chrome.addBasePath(API_BASE_URL);
+ const apiBaseUrl = core.http.basePath.prepend(API_BASE_URL);
const downloadLink = `${apiBaseUrl}/jobs/download/${jobId}`;
return downloadLink;
diff --git a/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.d.ts b/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.d.ts
deleted file mode 100644
index 3eacc3046e15a..0000000000000
--- a/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.d.ts
+++ /dev/null
@@ -1,13 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-declare class JobCompletionNotifications {
- public add(jobId: string): void;
-}
-
-declare const jobCompletionNotifications: JobCompletionNotifications;
-
-export { jobCompletionNotifications };
diff --git a/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.js b/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.js
deleted file mode 100644
index 786082638757e..0000000000000
--- a/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.js
+++ /dev/null
@@ -1,35 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../common/constants';
-
-export const jobCompletionNotifications = {
- add(jobId) {
- const jobs = this.getAll();
- jobs.push(jobId);
- this._set(jobs);
- },
-
- getAll() {
- const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY);
- return sessionValue ? JSON.parse(sessionValue) : [];
- },
-
- remove(jobId) {
- const jobs = this.getAll();
- const index = jobs.indexOf(jobId);
- if (!index) {
- throw new Error('Unable to find job to remove it');
- }
-
- jobs.splice(index, 1);
- this._set(jobs);
- },
-
- _set(jobs) {
- sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobs));
- },
-};
diff --git a/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.ts b/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.ts
new file mode 100644
index 0000000000000..3a61bc1e5a044
--- /dev/null
+++ b/x-pack/legacy/plugins/reporting/public/lib/job_completion_notifications.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../common/constants';
+
+type jobId = string;
+
+const set = (jobs: any) => {
+ sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobs));
+};
+
+const getAll = () => {
+ const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY);
+ return sessionValue ? JSON.parse(sessionValue) : [];
+};
+
+export const add = (jobId: jobId) => {
+ const jobs = getAll();
+ jobs.push(jobId);
+ set(jobs);
+};
+
+export const remove = (jobId: jobId) => {
+ const jobs = getAll();
+ const index = jobs.indexOf(jobId);
+
+ if (!index) {
+ throw new Error('Unable to find job to remove it');
+ }
+
+ jobs.splice(index, 1);
+ set(jobs);
+};
diff --git a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts b/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts
index 0f68c56a18bf6..281a2e1cdf9a5 100644
--- a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts
+++ b/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts
@@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { kfetch } from 'ui/kfetch';
+import { npStart } from 'ui/new_platform';
+import { API_LIST_URL } from '../../common/constants';
-const API_BASE_URL = '/api/reporting/jobs';
+const { core } = npStart;
export interface JobQueueEntry {
_id: string;
@@ -52,40 +53,33 @@ export interface JobInfo {
}
class JobQueueClient {
- public list = (page = 0, jobIds?: string[]): Promise => {
+ public list = (page = 0, jobIds: string[] = []): Promise => {
const query = { page } as any;
- if (jobIds && jobIds.length > 0) {
+ if (jobIds.length > 0) {
// Only getting the first 10, to prevent URL overflows
query.ids = jobIds.slice(0, 10).join(',');
}
- return kfetch({
- method: 'GET',
- pathname: `${API_BASE_URL}/list`,
+
+ return core.http.get(`${API_LIST_URL}/list`, {
query,
asSystemRequest: true,
});
};
public total(): Promise {
- return kfetch({
- method: 'GET',
- pathname: `${API_BASE_URL}/count`,
+ return core.http.get(`${API_LIST_URL}/count`, {
asSystemRequest: true,
});
}
public getContent(jobId: string): Promise {
- return kfetch({
- method: 'GET',
- pathname: `${API_BASE_URL}/output/${jobId}`,
+ return core.http.get(`${API_LIST_URL}/output/${jobId}`, {
asSystemRequest: true,
});
}
public getInfo(jobId: string): Promise {
- return kfetch({
- method: 'GET',
- pathname: `${API_BASE_URL}/info/${jobId}`,
+ return core.http.get(`${API_LIST_URL}/info/${jobId}`, {
asSystemRequest: true,
});
}
diff --git a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts b/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts
index b9574dfa457f3..9056c7967b4a8 100644
--- a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts
+++ b/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts
@@ -4,13 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { kfetch } from 'ui/kfetch';
+import { npStart } from 'ui/new_platform';
+import querystring from 'querystring';
+
+const { core } = npStart;
// @ts-ignore
import rison from 'rison-node';
-import chrome from 'ui/chrome';
-import { QueryString } from 'ui/utils/query_string';
-import { jobCompletionNotifications } from './job_completion_notifications';
+import { add } from './job_completion_notifications';
const API_BASE_URL = '/api/reporting/generate';
@@ -18,26 +19,22 @@ interface JobParams {
[paramName: string]: any;
}
-class ReportingClient {
- public getReportingJobPath = (exportType: string, jobParams: JobParams) => {
- return `${chrome.addBasePath(API_BASE_URL)}/${exportType}?${QueryString.param(
- 'jobParams',
- rison.encode(jobParams)
- )}`;
- };
-
- public createReportingJob = async (exportType: string, jobParams: any) => {
- const jobParamsRison = rison.encode(jobParams);
- const resp = await kfetch({
- method: 'POST',
- pathname: `${API_BASE_URL}/${exportType}`,
- body: JSON.stringify({
- jobParams: jobParamsRison,
- }),
- });
- jobCompletionNotifications.add(resp.job.id);
- return resp;
- };
-}
+export const getReportingJobPath = (exportType: string, jobParams: JobParams) => {
+ const params = querystring.stringify({ jobParams: rison.encode(jobParams) });
+
+ return `${core.http.basePath.prepend(API_BASE_URL)}/${exportType}?${params}`;
+};
+
+export const createReportingJob = async (exportType: string, jobParams: any) => {
+ const jobParamsRison = rison.encode(jobParams);
+ const resp = await core.http.post(`${API_BASE_URL}/${exportType}`, {
+ method: 'POST',
+ body: JSON.stringify({
+ jobParams: jobParamsRison,
+ }),
+ });
+
+ add(resp.job.id);
-export const reportingClient = new ReportingClient();
+ return resp;
+};
diff --git a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx
index 95ca1792a7eb1..fde053f8dbdfc 100644
--- a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx
+++ b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx
@@ -7,11 +7,7 @@ import dateMath from '@elastic/datemath';
import { i18n } from '@kbn/i18n';
import moment from 'moment-timezone';
-import { kfetch } from 'ui/kfetch';
-import { toastNotifications } from 'ui/notify';
-import chrome from 'ui/chrome';
-
-import { npSetup } from 'ui/new_platform';
+import { npSetup, npStart } from 'ui/new_platform';
import { IAction, IncompatibleActionError } from '../../../../../../src/plugins/ui_actions/public';
import {
@@ -22,11 +18,9 @@ import {
import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/constants';
import { ISearchEmbeddable } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types';
-import { API_BASE_URL_V1 } from '../../common/constants';
-
-const API_BASE_URL = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`;
+import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants';
-const CSV_REPORTING_ACTION = 'downloadCsvReport';
+const { core } = npStart;
function isSavedSearchEmbeddable(
embeddable: IEmbeddable | ISearchEmbeddable
@@ -71,12 +65,6 @@ class GetCsvReportPanelAction implements IAction {
}
public isCompatible = async (context: ActionContext) => {
- const enablePanelActionDownload = chrome.getInjected('enablePanelActionDownload');
-
- if (!enablePanelActionDownload) {
- return false;
- }
-
const { embeddable } = context;
return embeddable.getInput().viewMode !== ViewMode.EDIT && embeddable.type === 'search';
@@ -100,7 +88,7 @@ class GetCsvReportPanelAction implements IAction {
const searchEmbeddable = embeddable;
const searchRequestBody = await this.getSearchRequestBody({ searchEmbeddable });
const state = _.pick(searchRequestBody, ['sort', 'docvalue_fields', 'query']);
- const kibanaTimezone = chrome.getUiSettingsClient().get('dateFormat:tz');
+ const kibanaTimezone = core.uiSettings.get('dateFormat:tz');
const id = `search:${embeddable.getSavedSearch().id}`;
const filename = embeddable.getTitle();
@@ -125,7 +113,7 @@ class GetCsvReportPanelAction implements IAction {
this.isDownloading = true;
- toastNotifications.addSuccess({
+ core.notifications.toasts.addSuccess({
title: i18n.translate('xpack.reporting.dashboard.csvDownloadStartedTitle', {
defaultMessage: `CSV Download Started`,
}),
@@ -135,7 +123,8 @@ class GetCsvReportPanelAction implements IAction {
'data-test-subj': 'csvDownloadStarted',
});
- await kfetch({ method: 'POST', pathname: `${API_BASE_URL}/${id}`, body })
+ await core.http
+ .post(`${API_GENERATE_IMMEDIATE}/${id}`, { body })
.then((rawResponse: string) => {
this.isDownloading = false;
@@ -162,7 +151,7 @@ class GetCsvReportPanelAction implements IAction {
private onGenerationFail(error: Error) {
this.isDownloading = false;
- toastNotifications.addDanger({
+ core.notifications.toasts.addDanger({
title: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadTitle', {
defaultMessage: `CSV download failed`,
}),
@@ -175,5 +164,6 @@ class GetCsvReportPanelAction implements IAction {
}
const action = new GetCsvReportPanelAction();
+
npSetup.plugins.uiActions.registerAction(action);
npSetup.plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, action.id);
diff --git a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx
index fb5e74664e6c6..8e0da6a69225e 100644
--- a/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx
+++ b/x-pack/legacy/plugins/reporting/public/share_context_menu/register_reporting.tsx
@@ -14,6 +14,8 @@ import chrome from 'ui/chrome';
import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content';
import { ShareContext } from '../../../../../../src/plugins/share/public';
+const { core } = npSetup;
+
async function reportingProvider() {
const injector = await chrome.dangerouslyGetActiveInjector();
const getShareMenuItems = ({
@@ -35,12 +37,15 @@ async function reportingProvider() {
const getReportingJobParams = () => {
// Replace hashes with original RISON values.
- const relativeUrl = shareableUrl.replace(window.location.origin + chrome.getBasePath(), '');
+ const relativeUrl = shareableUrl.replace(
+ window.location.origin + core.http.basePath.get(),
+ ''
+ );
const browserTimezone =
- chrome.getUiSettingsClient().get('dateFormat:tz') === 'Browser'
+ core.uiSettings.get('dateFormat:tz') === 'Browser'
? moment.tz.guess()
- : chrome.getUiSettingsClient().get('dateFormat:tz');
+ : core.uiSettings.get('dateFormat:tz');
return {
...sharingData,
@@ -52,12 +57,15 @@ async function reportingProvider() {
const getPngJobParams = () => {
// Replace hashes with original RISON values.
- const relativeUrl = shareableUrl.replace(window.location.origin + chrome.getBasePath(), '');
+ const relativeUrl = shareableUrl.replace(
+ window.location.origin + core.http.basePath.get(),
+ ''
+ );
const browserTimezone =
- chrome.getUiSettingsClient().get('dateFormat:tz') === 'Browser'
+ core.uiSettings.get('dateFormat:tz') === 'Browser'
? moment.tz.guess()
- : chrome.getUiSettingsClient().get('dateFormat:tz');
+ : core.uiSettings.get('dateFormat:tz');
return {
...sharingData,
diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts
index 40cf315a78cbb..567838391d2e7 100644
--- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts
+++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts
@@ -5,13 +5,14 @@
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-// @ts-ignore untyped module
-import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants';
import { ServerFacade, ExportTypesRegistry, ESCallCluster } from '../../types';
import { KIBANA_REPORTING_TYPE } from '../../common/constants';
import { getReportingUsage } from './get_reporting_usage';
import { RangeStats } from './types';
+// places the reporting data as kibana stats
+const METATYPE = 'kibana_stats';
+
/*
* @param {Object} server
* @return {Object} kibana usage stats type collection object
@@ -35,7 +36,7 @@ export function getReportingUsageCollector(
*/
formatForBulkUpload: (result: RangeStats) => {
return {
- type: KIBANA_STATS_TYPE_MONITORING,
+ type: METATYPE,
payload: {
usage: {
xpack: {
diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx
index 4d92e8cb1335d..85e2b3b3fe384 100644
--- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx
@@ -16,7 +16,7 @@ const suggestion: autocomplete.QuerySuggestion = {
end: 3,
start: 1,
text: 'Text...',
- type: 'value',
+ type: autocomplete.QuerySuggestionsTypes.Value,
};
storiesOf('components/SuggestionItem', module).add('example', () => (
diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx
index ef16f79a4b83c..552aaa5889719 100644
--- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx
@@ -18,7 +18,7 @@ import { AutocompleteField } from '.';
const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [
{
- type: 'field',
+ type: autocomplete.QuerySuggestionsTypes.Field,
text: 'agent.ephemeral_id ',
description:
'Filter results that contain agent.ephemeral_id
',
@@ -26,7 +26,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [
end: 1,
},
{
- type: 'field',
+ type: autocomplete.QuerySuggestionsTypes.Field,
text: 'agent.hostname ',
description:
'Filter results that contain agent.hostname
',
@@ -34,7 +34,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [
end: 1,
},
{
- type: 'field',
+ type: autocomplete.QuerySuggestionsTypes.Field,
text: 'agent.id ',
description:
'Filter results that contain agent.id
',
@@ -42,7 +42,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [
end: 1,
},
{
- type: 'field',
+ type: autocomplete.QuerySuggestionsTypes.Field,
text: 'agent.name ',
description:
'Filter results that contain agent.name
',
@@ -50,7 +50,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [
end: 1,
},
{
- type: 'field',
+ type: autocomplete.QuerySuggestionsTypes.Field,
text: 'agent.type ',
description:
'Filter results that contain agent.type
',
@@ -58,7 +58,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [
end: 1,
},
{
- type: 'field',
+ type: autocomplete.QuerySuggestionsTypes.Field,
text: 'agent.version ',
description:
'Filter results that contain agent.version
',
@@ -66,7 +66,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [
end: 1,
},
{
- type: 'field',
+ type: autocomplete.QuerySuggestionsTypes.Field,
text: 'agent.test1 ',
description:
'Filter results that contain agent.test1
',
@@ -74,7 +74,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [
end: 1,
},
{
- type: 'field',
+ type: autocomplete.QuerySuggestionsTypes.Field,
text: 'agent.test2 ',
description:
'Filter results that contain agent.test2
',
@@ -82,7 +82,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [
end: 1,
},
{
- type: 'field',
+ type: autocomplete.QuerySuggestionsTypes.Field,
text: 'agent.test3 ',
description:
'Filter results that contain agent.test3
',
@@ -90,7 +90,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [
end: 1,
},
{
- type: 'field',
+ type: autocomplete.QuerySuggestionsTypes.Field,
text: 'agent.test4 ',
description:
'Filter results that contain agent.test4
',
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap
index 17588ae53ed00..ff63b3695fb8d 100644
--- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap
@@ -2,7 +2,7 @@
exports[`MonitorStatusBar component renders duration in ms, not us 1`] = `
-
-
+
+
-
+
-
-
-
+
+
+
+
+
-
+
`;
exports[`LocationMap component renders correctly against snapshot 1`] = `
-
-
+
+
-
+
-
-
-
-
+
+
+
+
+
+
-
+
`;
exports[`LocationMap component renders named locations that have missing geo data 1`] = `
-
-
+
+
-
+
-
-
-
-
+
+
+
+
+
+
-
+
`;
exports[`LocationMap component shows warning if geo information is missing 1`] = `
-
-
+
+
-
+
-
-
-
-
+
+
+
+
+
+
-
+
`;
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap
index 7862fe03163dc..46cb8c94ed6e3 100644
--- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap
@@ -1,13 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocationMissingWarning component renders correctly against snapshot 1`] = `
+.c0 {
+ margin-left: auto;
+ margin-bottom: 3px;
+}
+
-
- Geo Information Missing
+
}
closePopover={[Function]}
@@ -120,6 +123,6 @@ exports[`LocationMissingWarning component shallow render correctly against snaps
-
+
`;
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap
index 437150083f76f..e4fe0a2725db3 100644
--- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap
@@ -89,12 +89,20 @@ exports[`LocationStatusTags component renders when all locations are down 1`] =
.c1 {
margin-bottom: 5px;
+ white-space: nowrap;
}
.c0 {
- padding: 10px;
max-height: 229px;
overflow: hidden;
+ margin-top: auto;
+}
+
+@media (max-width:830px) {
+ .c1 {
+ display: inline-block;
+ margin-right: 16px;
+ }
}
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx
index c93e16d0a080b..8b9e410b0de79 100644
--- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx
@@ -6,7 +6,7 @@
import React from 'react';
import styled from 'styled-components';
-import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary, EuiHideFor } from '@elastic/eui';
import { LocationStatusTags } from './location_status_tags';
import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map';
import { MonitorLocations, MonitorLocation } from '../../../../common/runtime_types';
@@ -18,7 +18,28 @@ import { LocationMissingWarning } from './location_missing';
const MapPanel = styled.div`
height: 240px;
width: 520px;
- margin-right: 20px;
+ @media (min-width: 1300px) {
+ margin-right: 20px;
+ }
+ @media (max-width: 574px) {
+ height: 250px;
+ width: 100%;
+ margin-right: 0;
+ }
+`;
+
+const EuiFlexItemTags = styled(EuiFlexItem)`
+ padding-top: 5px;
+ @media (max-width: 850px) {
+ order: 1;
+ text-align: center;
+ }
+`;
+
+const FlexGroup = styled(EuiFlexGroup)`
+ @media (max-width: 850px) {
+ justify-content: center;
+ }
`;
interface LocationMapProps {
@@ -52,19 +73,22 @@ export const LocationMap = ({ monitorLocations }: LocationMapProps) => {
}
});
}
+
return (
-
-
+
+
+
+
+
+ {isGeoInfoMissing && }
+
+
+
+
-
- {isGeoInfoMissing && }
-
-
-
-
-
+
);
};
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_missing.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_missing.tsx
index f8efe42d0ce71..a20889f6cc653 100644
--- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_missing.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_missing.tsx
@@ -14,9 +14,15 @@ import {
EuiText,
EuiCode,
} from '@elastic/eui';
+import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { LocationLink } from '../monitor_list/monitor_list_drawer';
+const EuiPopoverRight = styled(EuiFlexItem)`
+ margin-left: auto;
+ margin-bottom: 3px;
+`;
+
export const LocationMissingWarning = () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@@ -26,13 +32,20 @@ export const LocationMissingWarning = () => {
const button = (
- Geo Information Missing
+
);
return (
-
-
+
+
{
-
+
);
};
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx
index e2855f78262e7..6955bf198eae0 100644
--- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx
@@ -23,13 +23,18 @@ const TextStyle = styled.div`
const BadgeItem = styled.div`
margin-bottom: 5px;
+ white-space: nowrap;
+ @media (max-width: 830px) {
+ display: inline-block;
+ margin-right: 16px;
+ }
`;
// Set height so that it remains within panel, enough height to display 7 locations tags
const TagContainer = styled.div`
- padding: 10px;
max-height: 229px;
overflow: hidden;
+ margin-top: auto;
`;
const OtherLocationsDiv = styled.div`
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx
index 57ca909ffde55..2524039829add 100644
--- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx
@@ -52,7 +52,7 @@ export const MonitorStatusBarComponent: React.FC
= ({
const full = monitorStatus?.url?.full ?? '';
return (
-
+
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx
index 63abb6fc4823c..29bd8eb3a7183 100644
--- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx
@@ -6,6 +6,7 @@
import React, { useContext, useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
+import styled from 'styled-components';
import { LocationMap } from '../location_map';
import { MonitorStatusBar } from './monitor_status_bar';
import { UptimeRefreshContext } from '../../../contexts';
@@ -19,6 +20,12 @@ interface MonitorStatusBarProps {
dateEnd: any;
}
+const WrapFlexItem = styled(EuiFlexItem)`
+ @media (max-width: 1150px) {
+ width: 100%;
+ }
+`;
+
export const MonitorStatusDetailsComponent = ({
monitorId,
variables,
@@ -54,7 +61,7 @@ export const MonitorStatusDetailsComponent = ({
return (
-
+
-
+
-
+
);
diff --git a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx b/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx
index e8705a2eb8eb3..7c3f80d4beb98 100644
--- a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx
+++ b/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx
@@ -67,7 +67,7 @@ export const PageHeaderComponent = ({ monitorStatus, setBreadcrumbs }: PageHeade
return (
<>
-
+
{headerText}
diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts
index 04a38972401ed..be14554f128c3 100644
--- a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts
+++ b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts
@@ -120,4 +120,82 @@ describe('test endpoint route', () => {
expect(endpointResultList.request_page_index).toEqual(10);
expect(endpointResultList.request_page_size).toEqual(10);
});
+
+ describe('Endpoint Details route', () => {
+ it('should return 404 on no results', async () => {
+ const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } });
+ mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
+ Promise.resolve({
+ took: 3,
+ timed_out: false,
+ _shards: {
+ total: 1,
+ successful: 1,
+ skipped: 0,
+ failed: 0,
+ },
+ hits: {
+ total: {
+ value: 9,
+ relation: 'eq',
+ },
+ max_score: null,
+ hits: [],
+ },
+ })
+ );
+ [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
+ path.startsWith('/api/endpoint/endpoints')
+ )!;
+
+ await routeHandler(
+ ({
+ core: {
+ elasticsearch: {
+ dataClient: mockScopedClient,
+ },
+ },
+ } as unknown) as RequestHandlerContext,
+ mockRequest,
+ mockResponse
+ );
+
+ expect(mockScopedClient.callAsCurrentUser).toBeCalled();
+ expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(mockResponse.notFound).toBeCalled();
+ const message = mockResponse.notFound.mock.calls[0][0]?.body;
+ expect(message).toEqual('Endpoint Not Found');
+ });
+
+ it('should return a single endpoint', async () => {
+ const mockRequest = httpServerMock.createKibanaRequest({
+ params: { id: (data as any).hits.hits[0]._id },
+ });
+ const response: SearchResponse = (data as unknown) as SearchResponse<
+ EndpointMetadata
+ >;
+ mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
+ [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
+ path.startsWith('/api/endpoint/endpoints')
+ )!;
+
+ await routeHandler(
+ ({
+ core: {
+ elasticsearch: {
+ dataClient: mockScopedClient,
+ },
+ },
+ } as unknown) as RequestHandlerContext,
+ mockRequest,
+ mockResponse
+ );
+
+ expect(mockScopedClient.callAsCurrentUser).toBeCalled();
+ expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(mockResponse.ok).toBeCalled();
+ const result = mockResponse.ok.mock.calls[0][0]?.body as EndpointMetadata;
+ expect(result).toHaveProperty('endpoint');
+ });
+ });
});
diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.ts b/x-pack/plugins/endpoint/server/routes/endpoints.ts
index 4fc3e653f9426..24ad8e3941f5e 100644
--- a/x-pack/plugins/endpoint/server/routes/endpoints.ts
+++ b/x-pack/plugins/endpoint/server/routes/endpoints.ts
@@ -8,7 +8,10 @@ import { IRouter } from 'kibana/server';
import { SearchResponse } from 'elasticsearch';
import { schema } from '@kbn/config-schema';
-import { kibanaRequestToEndpointListQuery } from '../services/endpoint/endpoint_query_builders';
+import {
+ kibanaRequestToEndpointListQuery,
+ kibanaRequestToEndpointFetchQuery,
+} from '../services/endpoint/endpoint_query_builders';
import { EndpointMetadata, EndpointResultList } from '../../common/types';
import { EndpointAppContext } from '../types';
@@ -51,6 +54,33 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
}
}
);
+
+ router.get(
+ {
+ path: '/api/endpoint/endpoints/{id}',
+ validate: {
+ params: schema.object({ id: schema.string() }),
+ },
+ options: { authRequired: true },
+ },
+ async (context, req, res) => {
+ try {
+ const query = kibanaRequestToEndpointFetchQuery(req, endpointAppContext);
+ const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser(
+ 'search',
+ query
+ )) as SearchResponse;
+
+ if (response.hits.hits.length === 0) {
+ return res.notFound({ body: 'Endpoint Not Found' });
+ }
+
+ return res.ok({ body: response.hits.hits[0]._source });
+ } catch (err) {
+ return res.internalError({ body: err });
+ }
+ }
+ );
}
function mapToEndpointResultList(
diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts
index 3c931a251d697..e453f777fbd50 100644
--- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts
+++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts
@@ -5,10 +5,13 @@
*/
import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks';
import { EndpointConfigSchema } from '../../config';
-import { kibanaRequestToEndpointListQuery } from './endpoint_query_builders';
+import {
+ kibanaRequestToEndpointListQuery,
+ kibanaRequestToEndpointFetchQuery,
+} from './endpoint_query_builders';
-describe('test query builder', () => {
- describe('test query builder request processing', () => {
+describe('query builder', () => {
+ describe('EndpointListQuery', () => {
it('test default query params for all endpoints when no params or body is provided', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
body: {},
@@ -51,4 +54,27 @@ describe('test query builder', () => {
} as Record);
});
});
+
+ describe('EndpointFetchQuery', () => {
+ it('searches for the correct ID', () => {
+ const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899';
+ const mockRequest = httpServerMock.createKibanaRequest({
+ params: {
+ id: mockID,
+ },
+ });
+ const query = kibanaRequestToEndpointFetchQuery(mockRequest, {
+ logFactory: loggingServiceMock.create(),
+ config: () => Promise.resolve(EndpointConfigSchema.validate({})),
+ });
+ expect(query).toEqual({
+ body: {
+ query: { match: { 'host.id.keyword': mockID } },
+ sort: [{ 'event.created': { order: 'desc' } }],
+ size: 1,
+ },
+ index: 'endpoint-agent*',
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts
index 102c268cf9ec4..b4f295a64b6ea 100644
--- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts
+++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts
@@ -65,3 +65,27 @@ async function getPagingProperties(
pageIndex: pagingProperties.page_index || config.endpointResultListDefaultFirstPageIndex,
};
}
+
+export const kibanaRequestToEndpointFetchQuery = (
+ request: KibanaRequest,
+ endpointAppContext: EndpointAppContext
+) => {
+ return {
+ body: {
+ query: {
+ match: {
+ 'host.id.keyword': request.params.id,
+ },
+ },
+ sort: [
+ {
+ 'event.created': {
+ order: 'desc',
+ },
+ },
+ ],
+ size: 1,
+ },
+ index: EndpointAppConstants.ENDPOINT_INDEX_NAME,
+ };
+};