diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md index 1eaf00c7a678d64..6229aeb9238e8d9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md @@ -16,6 +16,7 @@ Note that when generating absolute urls, the origin (protocol, host and port) ar getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean; + deepLinkId?: string; }): string; ``` @@ -24,7 +25,7 @@ getUrlForApp(appId: string, options?: { | Parameter | Type | Description | | --- | --- | --- | | appId | string | | -| options | {
path?: string;
absolute?: boolean;
} | | +| options | {
path?: string;
absolute?: boolean;
deepLinkId?: string;
} | | Returns: diff --git a/docs/discover/search-sessions.asciidoc b/docs/discover/search-sessions.asciidoc index fec1b8b26dd741a..b503e8cfba3b404 100644 --- a/docs/discover/search-sessions.asciidoc +++ b/docs/discover/search-sessions.asciidoc @@ -68,3 +68,19 @@ behaves differently: * Relative dates are converted to absolute dates. * Panning and zooming is disabled for maps. * Changing a filter, query, or drilldown starts a new search session, which can be slow. + +[float] +==== Limitations + +Certain visualization features do not fully support background search sessions yet. If a dashboard using these features gets restored, +all panels using unsupported features won't load immediately, but instead send out additional data requests which can take a while to complete. +In this case a warning *Your search session is still running* will be shown. + +You can either wait for these additional requests to complete or come back to the dashboard later when all data requests have been finished. + +A panel on a dashboard can behave like this if one of the following features is used: +* *Lens* - A *top values* dimension with an enabled setting *Group other values as "Other"* (configurable in the *Advanced* section of the dimension) +* *Lens* - An *intervals* dimension is used +* *Aggregation based* visualizations - A *terms* aggregation is used with an enabled setting *Group other values in separate bucket* +* *Aggregation based* visualizations - A *histogram* aggregation is used +* *Maps* - Layers using joins, blended layers or tracks layers are used diff --git a/docs/user/alerting/domain-specific-rules.asciidoc b/docs/user/alerting/domain-specific-rules.asciidoc deleted file mode 100644 index f509f9e5288234f..000000000000000 --- a/docs/user/alerting/domain-specific-rules.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[role="xpack"] -[[domain-specific-rules]] -== Domain-specific rules - -For domain-specific rules, refer to the documentation for that app. -{kib} supports these rules: - -* {observability-guide}/create-alerts.html[Observability rules] -* {security-guide}/prebuilt-rules.html[Security rules] -* <> -* {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[] - -[NOTE] -============================================== -Some rule types are subscription features, while others are free features. -For a comparison of the Elastic subscription levels, -see {subscriptions}[the subscription page]. -============================================== - -include::map-rules/geo-rule-types.asciidoc[] diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 68cf3ee070b0893..9ab6a2dc46ebf2d 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -3,6 +3,5 @@ include::alerting-setup.asciidoc[] include::create-and-manage-rules.asciidoc[] include::defining-rules.asciidoc[] include::rule-management.asciidoc[] -include::stack-rules.asciidoc[] -include::domain-specific-rules.asciidoc[] +include::rule-types.asciidoc[] include::alerting-troubleshooting.asciidoc[] diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc new file mode 100644 index 000000000000000..bb840014fe80fb4 --- /dev/null +++ b/docs/user/alerting/rule-types.asciidoc @@ -0,0 +1,56 @@ +[role="xpack"] +[[rule-types]] +== Rule types + +A rule is a set of <>, <>, and <> that enable notifications. {kib} provides two types of rules: rules specific to the Elastic Stack and rules specific to a domain. + +[NOTE] +============================================== +Some rule types are subscription features, while others are free features. +For a comparison of the Elastic subscription levels, +see {subscriptions}[the subscription page]. +============================================== + +[float] +[[stack-rules]] +=== Stack rules + +<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information. + +[cols="2*<"] +|=== + +| <> +| Aggregate field values from documents using {es} queries, compare them to threshold values, and schedule actions to run when the thresholds are met. + +| <> +| Run a user-configured {es} query, compare the number of matches to a configured threshold, and schedule actions to run when the threshold condition is met. + +|=== + +[float] +[[domain-specific-rules]] +=== Domain rules + +Domain rules are registered by *Observability*, *Security*, <> and <>. + +[cols="2*<"] +|=== + +| {observability-guide}/create-alerts.html[Observability rules] +| Detect complex conditions in the *Logs*, *Metrics*, and *Uptime* apps. + +| {security-guide}/prebuilt-rules.html[Security rules] +| Detect suspicous source events with pre-built or custom rules and create alerts when a rule’s conditions are met. + +| <> +| Run an {es} query to determine if any documents are currently contained in any boundaries from a specified boundary index and generate alerts when a rule's conditions are met. + +| {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[] +| Run scheduled checks on an anomaly detection job to detect anomalies with certain conditions. If an anomaly meets the conditions, an alert is created and the associated action is triggered. + +|=== + +include::rule-types/index-threshold.asciidoc[] +include::rule-types/es-query.asciidoc[] +include::rule-types/geo-rule-types.asciidoc[] diff --git a/docs/user/alerting/stack-rules/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc similarity index 100% rename from docs/user/alerting/stack-rules/es-query.asciidoc rename to docs/user/alerting/rule-types/es-query.asciidoc diff --git a/docs/user/alerting/map-rules/geo-rule-types.asciidoc b/docs/user/alerting/rule-types/geo-rule-types.asciidoc similarity index 74% rename from docs/user/alerting/map-rules/geo-rule-types.asciidoc rename to docs/user/alerting/rule-types/geo-rule-types.asciidoc index eee7b592522054a..244cf90c855a7e1 100644 --- a/docs/user/alerting/map-rules/geo-rule-types.asciidoc +++ b/docs/user/alerting/rule-types/geo-rule-types.asciidoc @@ -1,16 +1,14 @@ [role="xpack"] [[geo-alerting]] -=== Geo rule type +=== Tracking containment -Alerting now includes one additional stack rule: <>. - -As with other stack rules, you need `all` access to the *Stack Rules* feature -to be able to create and edit a geo rule. -See <> for more information on configuring roles that provide access to this feature. +<> offers the Tracking containment rule type which runs an {es} query over indices to determine whether any +documents are currently contained within any boundaries from the specified boundary index. +In the event that an entity is contained within a boundary, an alert may be generated. [float] -==== Geo alerting requirements -To create a *Tracking containment* rule, the following requirements must be present: +==== Requirements +To create a Tracking containment rule, the following requirements must be present: - *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field, and some form of entity identifier. An entity identifier is a `keyword` or `number` @@ -29,22 +27,12 @@ than the current time minus the amount of the interval. If data older than `now - ` is ingested, it won't trigger a rule. [float] -==== Creating a geo rule -Click the *Create* button in the <>. -Complete the <>. - -[role="screenshot"] -image::user/alerting/images/alert-types-tracking-select.png[Choosing a tracking rule type] +==== Create the rule -[float] -[[rule-type-tracking-containment]] -==== Tracking containment -The Tracking containment rule type runs an {es} query over indices, determining if any -documents are currently contained within any boundaries from the specified boundary index. -In the event that an entity is contained within a boundary, an alert may be generated. +Fill in the <>, then select Tracking containment. [float] -===== Defining the conditions +==== Define the conditions Tracking containment rules have 3 clauses that define the condition to detect, as well as 2 Kuery bars used to provide additional filtering context for each of the indices. @@ -61,6 +49,9 @@ Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_sha identifying boundaries, and an optional *Human-readable boundary name* for better alerting messages. +[float] +==== Add action + Conditions for how a rule is tracked can be specified uniquely for each individual action. A rule can be triggered either when a containment condition is met or when an entity is no longer contained. diff --git a/docs/user/alerting/stack-rules/index-threshold.asciidoc b/docs/user/alerting/rule-types/index-threshold.asciidoc similarity index 100% rename from docs/user/alerting/stack-rules/index-threshold.asciidoc rename to docs/user/alerting/rule-types/index-threshold.asciidoc diff --git a/docs/user/alerting/stack-rules.asciidoc b/docs/user/alerting/stack-rules.asciidoc deleted file mode 100644 index 483834c78806e23..000000000000000 --- a/docs/user/alerting/stack-rules.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[role="xpack"] -[[stack-rules]] -== Stack rule types - -Kibana provides two types of rules: - -* Stack rules, which are built into {kib} -* <>, which are registered by {kib} apps. - -{kib} provides two stack rules: - -* <> -* <> - -Users require the `all` privilege to access the *Stack Rules* feature and create and edit rules. -See <> for more information. - -[NOTE] -============================================== -Some rule types are subscription features, while others are free features. -For a comparison of the Elastic subscription levels, -see {subscriptions}[the subscription page]. -============================================== - - -include::stack-rules/index-threshold.asciidoc[] -include::stack-rules/es-query.asciidoc[] diff --git a/package.json b/package.json index 513352db3f81bb8..ff2f62f51308410 100644 --- a/package.json +++ b/package.json @@ -215,7 +215,6 @@ "cytoscape-dagre": "^2.2.2", "d3": "3.5.17", "d3-array": "1.2.4", - "d3-cloud": "1.2.5", "d3-scale": "1.0.7", "d3-shape": "^1.1.0", "d3-time": "^1.1.0", diff --git a/packages/kbn-test/src/jest/utils/router_helpers.tsx b/packages/kbn-test/src/jest/utils/router_helpers.tsx index e2245440274d190..85ef27488a4ce95 100644 --- a/packages/kbn-test/src/jest/utils/router_helpers.tsx +++ b/packages/kbn-test/src/jest/utils/router_helpers.tsx @@ -8,18 +8,39 @@ import React, { Component, ComponentType } from 'react'; import { MemoryRouter, Route, withRouter } from 'react-router-dom'; -import * as H from 'history'; +import { History, LocationDescriptor } from 'history'; -export const WithMemoryRouter = (initialEntries: string[] = ['/'], initialIndex: number = 0) => ( - WrappedComponent: ComponentType -) => (props: any) => ( +const stringifyPath = (path: LocationDescriptor): string => { + if (typeof path === 'string') { + return path; + } + + return path.pathname || '/'; +}; + +const locationDescriptorToRoutePath = ( + paths: LocationDescriptor | LocationDescriptor[] +): string | string[] => { + if (Array.isArray(paths)) { + return paths.map((path: LocationDescriptor) => { + return stringifyPath(path); + }); + } + + return stringifyPath(paths); +}; + +export const WithMemoryRouter = ( + initialEntries: LocationDescriptor[] = ['/'], + initialIndex: number = 0 +) => (WrappedComponent: ComponentType) => (props: any) => ( ); export const WithRoute = ( - componentRoutePath: string | string[] = '/', + componentRoutePath: LocationDescriptor | LocationDescriptor[] = ['/'], onRouter = (router: any) => {} ) => (WrappedComponent: ComponentType) => { // Create a class component that will catch the router @@ -40,16 +61,16 @@ export const WithRoute = ( return (props: any) => ( } /> ); }; interface Router { - history: Partial; + history: Partial; route: { - location: H.Location; + location: LocationDescriptor; }; } diff --git a/packages/kbn-test/src/jest/utils/testbed/types.ts b/packages/kbn-test/src/jest/utils/testbed/types.ts index fdc000215c4f197..bba504951c0bc6e 100644 --- a/packages/kbn-test/src/jest/utils/testbed/types.ts +++ b/packages/kbn-test/src/jest/utils/testbed/types.ts @@ -8,6 +8,7 @@ import { Store } from 'redux'; import { ReactWrapper } from 'enzyme'; +import { LocationDescriptor } from 'history'; export type SetupFunc = (props?: any) => TestBed | Promise>; @@ -161,11 +162,11 @@ export interface MemoryRouterConfig { /** Flag to add or not the `MemoryRouter`. If set to `false`, there won't be any router and the component won't be wrapped on a ``. */ wrapComponent?: boolean; /** The React Router **initial entries** setting ([see documentation](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md)) */ - initialEntries?: string[]; + initialEntries?: LocationDescriptor[]; /** The React Router **initial index** setting ([see documentation](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md)) */ initialIndex?: number; /** The route **path** for the mounted component (defaults to `"/"`) */ - componentRoutePath?: string | string[]; + componentRoutePath?: LocationDescriptor | LocationDescriptor[]; /** A callBack that will be called with the React Router instance once mounted */ onRouter?: (router: any) => void; } diff --git a/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts b/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts new file mode 100644 index 000000000000000..25651a0dd21902f --- /dev/null +++ b/packages/kbn-test/src/kbn_client/import_export/parse_archive.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { parseArchive } from './parse_archive'; + +jest.mock('fs/promises', () => ({ + readFile: jest.fn(), +})); + +const mockReadFile = jest.requireMock('fs/promises').readFile; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('parses archives with \\n', async () => { + mockReadFile.mockResolvedValue( + `{ + "foo": "abc" + }\n\n{ + "foo": "xyz" + }` + ); + + const archive = await parseArchive('mock'); + expect(archive).toMatchInlineSnapshot(` + Array [ + Object { + "foo": "abc", + }, + Object { + "foo": "xyz", + }, + ] + `); +}); + +it('parses archives with \\r\\n', async () => { + mockReadFile.mockResolvedValue( + `{ + "foo": "123" + }\r\n\r\n{ + "foo": "456" + }` + ); + + const archive = await parseArchive('mock'); + expect(archive).toMatchInlineSnapshot(` + Array [ + Object { + "foo": "123", + }, + Object { + "foo": "456", + }, + ] + `); +}); diff --git a/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts b/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts new file mode 100644 index 000000000000000..b6b85ba521525bf --- /dev/null +++ b/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Fs from 'fs/promises'; + +export interface SavedObject { + id: string; + type: string; + [key: string]: unknown; +} + +export async function parseArchive(path: string): Promise { + return (await Fs.readFile(path, 'utf-8')) + .split(/\r?\n\r?\n/) + .filter((line) => !!line) + .map((line) => JSON.parse(line)); +} diff --git a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts index 88953cdbaed7c98..4adae7d1cd031e8 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts @@ -16,25 +16,12 @@ import { ToolingLog, isAxiosResponseError, createFailError, REPO_ROOT } from '@k import { KbnClientRequester, uriencode, ReqOptions } from './kbn_client_requester'; import { KbnClientSavedObjects } from './kbn_client_saved_objects'; +import { parseArchive } from './import_export/parse_archive'; interface ImportApiResponse { success: boolean; [key: string]: unknown; } - -interface SavedObject { - id: string; - type: string; - [key: string]: unknown; -} - -async function parseArchive(path: string): Promise { - return (await Fs.readFile(path, 'utf-8')) - .split('\n\n') - .filter((line) => !!line) - .map((line) => JSON.parse(line)); -} - export class KbnClientImportExport { constructor( public readonly log: ToolingLog, diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 5658d3f62607729..3ed164088bf5c79 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -497,6 +497,56 @@ describe('#start()', () => { expect(getUrlForApp('app1', { path: 'deep/link///' })).toBe('/base-path/app/app1/deep/link'); }); + describe('deepLinkId option', () => { + it('ignores the deepLinkId parameter if it is unknown', async () => { + service.setup(setupDeps); + + service.setup(setupDeps); + const { getUrlForApp } = await service.start(startDeps); + + expect(getUrlForApp('app1', { deepLinkId: 'unkown-deep-link' })).toBe( + '/base-path/app/app1' + ); + }); + + it('creates URLs with deepLinkId parameter', async () => { + const { register } = service.setup(setupDeps); + + register( + Symbol(), + createApp({ + id: 'app1', + appRoute: '/custom/app-path', + deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }], + }) + ); + + const { getUrlForApp } = await service.start(startDeps); + + expect(getUrlForApp('app1', { deepLinkId: 'dl1' })).toBe( + '/base-path/custom/app-path/deep-link' + ); + }); + + it('creates URLs with deepLinkId and path parameters', async () => { + const { register } = service.setup(setupDeps); + + register( + Symbol(), + createApp({ + id: 'app1', + appRoute: '/custom/app-path', + deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }], + }) + ); + + const { getUrlForApp } = await service.start(startDeps); + expect(getUrlForApp('app1', { deepLinkId: 'dl1', path: 'foo/bar' })).toBe( + '/base-path/custom/app-path/deep-link/foo/bar' + ); + }); + }); + it('does not append trailing slash if hash is provided in path parameter', async () => { service.setup(setupDeps); const { getUrlForApp } = await service.start(startDeps); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 32d45b32c32ffd5..8c6090caabce193 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -282,8 +282,19 @@ export class ApplicationService { history: this.history!, getUrlForApp: ( appId, - { path, absolute = false }: { path?: string; absolute?: boolean } = {} + { + path, + absolute = false, + deepLinkId, + }: { path?: string; absolute?: boolean; deepLinkId?: string } = {} ) => { + if (deepLinkId) { + const deepLinkPath = getAppDeepLinkPath(availableMounters, appId, deepLinkId); + if (deepLinkPath) { + path = appendAppPath(deepLinkPath, path); + } + } + const relUrl = http.basePath.prepend(getAppUrl(availableMounters, appId, path)); return absolute ? relativeToAbsolute(relUrl) : relUrl; }, diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 60b0dbf158dd910..5803f2e3779abc9 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -780,7 +780,10 @@ export interface ApplicationStart { * @param options.path - optional path inside application to deep link to * @param options.absolute - if true, will returns an absolute url instead of a relative one */ - getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean }): string; + getUrlForApp( + appId: string, + options?: { path?: string; absolute?: boolean; deepLinkId?: string } + ): string; /** * An observable that emits the current application id and each subsequent id update. diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 53428edf4b345f2..06277d9351922c2 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -142,7 +142,7 @@ export class DocLinksService { dataStreams: `${ELASTICSEARCH_DOCS}data-streams.html`, indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`, indexSettings: `${ELASTICSEARCH_DOCS}index-modules.html#index-modules-settings`, - indexTemplates: `${ELASTICSEARCH_DOCS}indices-templates.html`, + indexTemplates: `${ELASTICSEARCH_DOCS}index-templates.html`, mapping: `${ELASTICSEARCH_DOCS}mapping.html`, mappingAnalyzer: `${ELASTICSEARCH_DOCS}analyzer.html`, mappingCoerce: `${ELASTICSEARCH_DOCS}coerce.html`, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 235110aeb4633c7..d3426b50f76143b 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -150,6 +150,7 @@ export interface ApplicationStart { getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean; + deepLinkId?: string; }): string; navigateToApp(appId: string, options?: NavigateToAppOptions): Promise; navigateToUrl(url: string): Promise; diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 69687f75f309828..feff425cc48edd3 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -834,8 +834,8 @@ describe('Execution', () => { expect((chain[0].arguments.val[0] as ExpressionAstExpression).chain[0].debug!.args).toEqual( { - name: 'foo', - value: 5, + name: ['foo'], + value: [5], } ); }); diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts index 0a9f022ce89cad7..cdcae61215fa423 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts @@ -9,6 +9,8 @@ import { functionWrapper } from './utils'; import { variableSet } from '../var_set'; import { ExecutionContext } from '../../../execution/types'; +import { createUnitTestExecutor } from '../../../test_helpers'; +import { first } from 'rxjs/operators'; describe('expression_functions', () => { describe('var_set', () => { @@ -32,21 +34,49 @@ describe('expression_functions', () => { }); it('updates a variable', () => { - const actual = fn(input, { name: 'test', value: 2 }, context); + const actual = fn(input, { name: ['test'], value: [2] }, context); expect(variables.test).toEqual(2); expect(actual).toEqual(input); }); it('sets a new variable', () => { - const actual = fn(input, { name: 'new', value: 3 }, context); + const actual = fn(input, { name: ['new'], value: [3] }, context); expect(variables.new).toEqual(3); expect(actual).toEqual(input); }); it('stores context if value is not set', () => { - const actual = fn(input, { name: 'test' }, context); + const actual = fn(input, { name: ['test'], value: [] }, context); expect(variables.test).toEqual(input); expect(actual).toEqual(input); }); + + it('sets multiple variables', () => { + const actual = fn(input, { name: ['new1', 'new2', 'new3'], value: [1, , 3] }, context); + expect(variables.new1).toEqual(1); + expect(variables.new2).toEqual(input); + expect(variables.new3).toEqual(3); + expect(actual).toEqual(input); + }); + + describe('running function thru executor', () => { + const executor = createUnitTestExecutor(); + executor.registerFunction(variableSet); + + it('sets the variables', async () => { + const vars = {}; + const result = await executor + .run('var_set name=test1 name=test2 value=1', 2, { variables: vars }) + .pipe(first()) + .toPromise(); + + expect(result).toEqual(2); + + expect(vars).toEqual({ + test1: 1, + test2: 2, + }); + }); + }); }); }); diff --git a/src/plugins/expressions/common/expression_functions/specs/var_set.ts b/src/plugins/expressions/common/expression_functions/specs/var_set.ts index 490c7781a01a1e6..f3ac6a2ab80d4a8 100644 --- a/src/plugins/expressions/common/expression_functions/specs/var_set.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var_set.ts @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; interface Arguments { - name: string; - value?: any; + name: string[]; + value: any[]; } export type ExpressionFunctionVarSet = ExpressionFunctionDefinition< @@ -31,12 +31,14 @@ export const variableSet: ExpressionFunctionVarSet = { types: ['string'], aliases: ['_'], required: true, + multi: true, help: i18n.translate('expressions.functions.varset.name.help', { defaultMessage: 'Specify the name of the variable.', }), }, value: { aliases: ['val'], + multi: true, help: i18n.translate('expressions.functions.varset.val.help', { defaultMessage: 'Specify the value for the variable. When unspecified, the input context is used.', @@ -45,7 +47,9 @@ export const variableSet: ExpressionFunctionVarSet = { }, fn(input, args, context) { const variables: Record = context.variables; - variables[args.name] = args.value === undefined ? input : args.value; + args.name.forEach((name, i) => { + variables[name] = args.value[i] === undefined ? input : args.value[i]; + }); return input; }, }; diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts index a12a2ff195211d4..267769d33fba2cc 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts @@ -280,7 +280,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[eCommerce] Top Selling Products', }), visState: - '{"title":"[eCommerce] Top Selling Products","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"products.product_name.keyword","size":7,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[eCommerce] Top Selling Products","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"products.product_name.keyword","size":7,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index 05a3d012d707c17..816322dbe5299cb 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -242,7 +242,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Destination Weather', }), visState: - '{"title":"[Flights] Destination Weather","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestWeather","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[Flights] Destination Weather","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestWeather","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 21248ac9d1dc0bc..38a9e4701441685 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -14,46 +14,46 @@ exports[`CreateIndexPatternWizard defaults to the loading state 1`] = ` exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = ` - -
- - + + - + } + showSystemIndices={true} + /> - -
- - + + - + } + showSystemIndices={true} + /> - -
- - + + - + } + /> - -
- - + + - + } + showSystemIndices={true} + /> - -
- - + + - + } + showSystemIndices={true} + /> } > -
- -

+ Create test index pattern - - - - Beta - - -

-
- + + + + + } + > +
-
- - -
+ Create test index pattern + + + + + + } + responsive={true} > -

- - multiple - , - "single": - filebeat-4-3-22 - , - "star": - filebeat-* - , - } - } +

+ - - An index pattern can match a single source, for example, - - - - - filebeat-4-3-22 - - - - - , or - - multiple - - data sources, - - + +
- - - filebeat-* - - - - - . - - -
- +

+ Create test index pattern + + + + Beta + + +

+ +
+
+
+ +
- - -

-
- - -
- -
- Test prompt -
-
+

+ + multiple + , + "single": + filebeat-4-3-22 + , + "star": + filebeat-* + , + } + } + > + + An index pattern can match a single source, for example, + + + + + filebeat-4-3-22 + + + + + , or + + multiple + + data sources, + + + + + filebeat-* + + + + + . + + +
+ + + +

+
+
+ +
+ +
+ Test prompt +
+
+
+ +
+
`; @@ -146,100 +203,145 @@ exports[`Header should render normally 1`] = ` } indexPatternName="test index pattern" > -
- -

+ Create test index pattern -

-
- + } + > +
-
- - -
+ Create test index pattern + + } + responsive={true} > -

- - multiple - , - "single": - filebeat-4-3-22 - , - "star": - filebeat-* - , - } - } +

+ - - An index pattern can match a single source, for example, - - - - - filebeat-4-3-22 - - - - - , or - - multiple - - data sources, - - + +
- - - filebeat-* - - - - - . - - -
- +

+ Create test index pattern +

+ +
+
+
+ +
- - -

-
- -
+

+ + multiple + , + "single": + filebeat-4-3-22 + , + "star": + filebeat-* + , + } + } + > + + An index pattern can match a single source, for example, + + + + + filebeat-4-3-22 + + + + + , or + + multiple + + data sources, + + + + + filebeat-* + + + + + . + + +
+ + + +

+
+ +
+ + +
+
`; @@ -254,99 +356,144 @@ exports[`Header should render without including system indices 1`] = ` } indexPatternName="test index pattern" > -
- -

+ Create test index pattern -

-
- + } + > +
-
- - -
+ Create test index pattern + + } + responsive={true} > -

- - multiple - , - "single": - filebeat-4-3-22 - , - "star": - filebeat-* - , - } - } +

+ - - An index pattern can match a single source, for example, - - - - - filebeat-4-3-22 - - - - - , or - - multiple - - data sources, - - + +
- - - filebeat-* - - - - - . - - -
- +

+ Create test index pattern +

+ +
+
+
+ +
- - -

-
- -
+

+ + multiple + , + "single": + filebeat-4-3-22 + , + "star": + filebeat-* + , + } + } + > + + An index pattern can match a single source, for example, + + + + + filebeat-4-3-22 + + + + + , or + + multiple + + data sources, + + + + + filebeat-* + + + + + . + + +
+ + + +

+
+ +
+ + +
+
`; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx index a7e3b2ded75dc68..c708bd3cac33e70 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx @@ -8,7 +8,7 @@ import React from 'react'; -import { EuiBetaBadge, EuiSpacer, EuiTitle, EuiText, EuiCode, EuiLink } from '@elastic/eui'; +import { EuiBetaBadge, EuiCode, EuiLink, EuiPageHeader, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -39,9 +39,9 @@ export const Header = ({ changeTitle(createIndexPatternHeader); return ( -
- -

+ {createIndexPatternHeader} {isBeta ? ( <> @@ -53,9 +53,10 @@ export const Header = ({ /> ) : null} -

-
- + + } + bottomBorder + >

) : null} -

+ ); }; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx index 633906feb785b4a..5bc53105dbcf87b 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx @@ -6,17 +6,12 @@ * Side Public License, v 1. */ -import React, { ReactElement, Component } from 'react'; - -import { - EuiGlobalToastList, - EuiGlobalToastListToast, - EuiPageContent, - EuiHorizontalRule, -} from '@elastic/eui'; +import React, { Component, ReactElement } from 'react'; + +import { EuiGlobalToastList, EuiGlobalToastListToast, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; import { DocLinksStart } from 'src/core/public'; import { StepIndexPattern } from './components/step_index_pattern'; import { StepTimeField } from './components/step_time_field'; @@ -227,9 +222,9 @@ export class CreateIndexPatternWizard extends Component< const initialQuery = new URLSearchParams(location.search).get('id') || undefined; return ( - + <> {header} - + - + ); } if (step === 2) { return ( - + <> {header} - + - + ); } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx index 5aa9853c5e766f0..0c0adc6dd502959 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx @@ -7,15 +7,15 @@ */ import React from 'react'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { IndexPattern, IndexPatternField } from '../../../../../../plugins/data/public'; import { useKibana } from '../../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../../types'; import { IndexHeader } from '../index_header'; -import { TAB_SCRIPTED_FIELDS, TAB_INDEXED_FIELDS } from '../constants'; +import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS } from '../constants'; import { FieldEditor } from '../../field_editor'; @@ -76,26 +76,18 @@ export const CreateEditField = withRouter( if (spec) { return ( - - - - - - - - - - + <> + + + + ); } else { return <>; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index e314c00bc8176f8..6609605da87d197 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -17,7 +17,6 @@ import { EuiText, EuiLink, EuiCallOut, - EuiPanel, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -145,15 +144,13 @@ export const EditIndexPattern = withRouter( const kibana = useKibana(); const docsUrl = kibana.services.docLinks!.links.elasticsearch.mapping; return ( - -
- - +
+ {showTagsSection && ( {Boolean(indexPattern.timeFieldName) && ( @@ -193,19 +190,19 @@ export const EditIndexPattern = withRouter( )} - - { - setFields(indexPattern.getNonScriptedFields()); - }} - /> -
- +
+ + { + setFields(indexPattern.getNonScriptedFields()); + }} + /> +
); } ); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx index 482cd574c8f1d65..c141c228a68f256 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiToolTip, EuiFlexItem, EuiTitle, EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonIcon, EuiPageHeader, EuiToolTip } from '@elastic/eui'; import { IIndexPattern } from 'src/plugins/data/public'; interface IndexHeaderProps { @@ -40,50 +40,42 @@ const removeTooltip = i18n.translate('indexPatternManagement.editIndexPattern.re defaultMessage: 'Remove index pattern.', }); -export function IndexHeader({ +export const IndexHeader: React.FC = ({ defaultIndex, indexPattern, setDefault, deleteIndexPatternClick, -}: IndexHeaderProps) { + children, +}) => { return ( - - - -

{indexPattern.title}

-
-
- - - {defaultIndex !== indexPattern.id && setDefault && ( - - - - - - )} - - {deleteIndexPatternClick && ( - - - - - - )} - - -
+ {indexPattern.title}} + rightSideItems={[ + defaultIndex !== indexPattern.id && setDefault && ( + + + + ), + deleteIndexPatternClick && ( + + + + ), + ].filter(Boolean)} + > + {children} + ); -} +}; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap index c5e6d1220d8bf8d..bc69fa29e690443 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap @@ -3,9 +3,11 @@ exports[`EmptyIndexPatternPrompt should render normally 1`] = ` diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap index 1310488c65fab86..957c94c80680d9f 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap @@ -4,9 +4,11 @@ exports[`EmptyState should render normally 1`] = ` diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx index 240e732752916cb..c05f6a1f193b7af 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx @@ -63,8 +63,10 @@ export const EmptyState = ({ diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index f018294f27c84af..6bd06528084ce9a 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -8,24 +8,20 @@ import { EuiBadge, + EuiBadgeGroup, EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, EuiInMemoryTable, + EuiPageHeader, EuiSpacer, - EuiText, - EuiBadgeGroup, - EuiPageContent, - EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; -import React, { useState, useEffect } from 'react'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { reactRouterNavigate, useKibana } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { CreateButton } from '../create_button'; -import { IndexPatternTableItem, IndexPatternCreationOption } from '../types'; +import { IndexPatternCreationOption, IndexPatternTableItem } from '../types'; import { getIndexPatterns } from '../utils'; import { getListBreadcrumbs } from '../breadcrumbs'; import { EmptyState } from './empty_state'; @@ -54,10 +50,6 @@ const search = { }, }; -const ariaRegion = i18n.translate('indexPatternManagement.editIndexPatternLiveRegionAriaLabel', { - defaultMessage: 'Index patterns', -}); - const title = i18n.translate('indexPatternManagement.indexPatternTable.title', { defaultMessage: 'Index patterns', }); @@ -197,25 +189,21 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { } return ( - - - - -

{title}

-
- - -

- -

-
-
- {createButton} -
- +
+ + } + bottomBorder + rightSideItems={[createButton]} + /> + + + { sorting={sorting} search={search} /> - +
); }; diff --git a/src/plugins/index_pattern_management/public/constants.ts b/src/plugins/index_pattern_management/public/constants.ts new file mode 100644 index 000000000000000..e5010d133f0f301 --- /dev/null +++ b/src/plugins/index_pattern_management/public/constants.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns'; diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 6c709fb14f08d73..7671a532d1cb862 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -19,14 +19,7 @@ import { } from './plugin'; import { IndexPatternManagmentContext } from './types'; -const createSetupContract = (): IndexPatternManagementSetup => ({ - creation: { - addCreationConfig: jest.fn(), - } as any, - list: { - addListConfig: jest.fn(), - } as any, -}); +const createSetupContract = (): IndexPatternManagementSetup => {}; const createStartContract = (): IndexPatternManagementStart => ({ creation: { diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index e3c156927bface6..610b3541620b00b 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -81,7 +81,10 @@ export class IndexPatternManagementPlugin }, }); - return this.indexPatternManagementService.setup({ httpClient: core.http }); + return this.indexPatternManagementService.setup({ + httpClient: core.http, + uiSettings: core.uiSettings, + }); } public start(core: CoreStart, plugins: IndexPatternManagementStartDependencies) { diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/index.js b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/index.ts similarity index 53% rename from x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/index.js rename to src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/index.ts index 1d9eff8227c0acd..d1fc2fa242eb1b2 100644 --- a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/index.js +++ b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ export { RollupPrompt } from './rollup_prompt'; diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/rollup_prompt.tsx similarity index 76% rename from x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js rename to src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/rollup_prompt.tsx index 9306ab082dff49d..81fcdaedb90c90a 100644 --- a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js +++ b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/rollup_prompt.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -14,7 +15,7 @@ export const RollupPrompt = () => (

{i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text', + 'indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text', { defaultMessage: "Kibana's support for rollup index patterns is in beta. You might encounter issues using " + @@ -25,7 +26,7 @@ export const RollupPrompt = () => (

{i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text', + 'indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text', { defaultMessage: 'You can match a rollup index pattern against one rollup index and zero or more regular ' + diff --git a/src/plugins/index_pattern_management/public/service/creation/index.ts b/src/plugins/index_pattern_management/public/service/creation/index.ts index 51610bc83e371bb..e1f464b01e5505d 100644 --- a/src/plugins/index_pattern_management/public/service/creation/index.ts +++ b/src/plugins/index_pattern_management/public/service/creation/index.ts @@ -8,3 +8,5 @@ export { IndexPatternCreationConfig, IndexPatternCreationOption } from './config'; export { IndexPatternCreationManager } from './manager'; +// @ts-ignore +export { RollupIndexPatternCreationConfig } from './rollup_creation_config'; diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js b/src/plugins/index_pattern_management/public/service/creation/rollup_creation_config.js similarity index 84% rename from x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js rename to src/plugins/index_pattern_management/public/service/creation/rollup_creation_config.js index 8e5203fca903477..2a85dfa01143c79 100644 --- a/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js +++ b/src/plugins/index_pattern_management/public/service/creation/rollup_creation_config.js @@ -1,43 +1,44 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { i18n } from '@kbn/i18n'; import { RollupPrompt } from './components/rollup_prompt'; -import { IndexPatternCreationConfig } from '../../../../../src/plugins/index_pattern_management/public'; +import { IndexPatternCreationConfig } from '.'; const rollupIndexPatternTypeName = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultTypeName', + 'indexPatternManagement.editRollupIndexPattern.createIndex.defaultTypeName', { defaultMessage: 'rollup index pattern' } ); const rollupIndexPatternButtonText = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultButtonText', + 'indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonText', { defaultMessage: 'Rollup index pattern' } ); const rollupIndexPatternButtonDescription = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultButtonDescription', + 'indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonDescription', { defaultMessage: 'Perform limited aggregations against summarized data' } ); const rollupIndexPatternNoMatchError = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.noMatchError', + 'indexPatternManagement.editRollupIndexPattern.createIndex.noMatchError', { defaultMessage: 'Rollup index pattern error: must match one rollup index' } ); const rollupIndexPatternTooManyMatchesError = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.tooManyMatchesError', + 'indexPatternManagement.editRollupIndexPattern.createIndex.tooManyMatchesError', { defaultMessage: 'Rollup index pattern error: can only match one rollup index' } ); const rollupIndexPatternIndexLabel = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.indexLabel', + 'indexPatternManagement.editRollupIndexPattern.createIndex.indexLabel', { defaultMessage: 'Rollup' } ); @@ -127,7 +128,7 @@ export class RollupIndexPatternCreationConfig extends IndexPatternCreationConfig if (error) { const errorMessage = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.uncaughtError', + 'indexPatternManagement.editRollupIndexPattern.createIndex.uncaughtError', { defaultMessage: 'Rollup index pattern error: {error}', values: { diff --git a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index f30ccfcb9f3ed79..19346dbf31d185d 100644 --- a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -6,11 +6,22 @@ * Side Public License, v 1. */ -import { HttpSetup } from '../../../../core/public'; -import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; -import { IndexPatternListManager, IndexPatternListConfig } from './list'; +import { HttpSetup, CoreSetup } from '../../../../core/public'; +import { + IndexPatternCreationManager, + IndexPatternCreationConfig, + RollupIndexPatternCreationConfig, +} from './creation'; +import { + IndexPatternListManager, + IndexPatternListConfig, + RollupIndexPatternListConfig, +} from './list'; + +import { CONFIG_ROLLUPS } from '../constants'; interface SetupDependencies { httpClient: HttpSetup; + uiSettings: CoreSetup['uiSettings']; } /** @@ -27,17 +38,17 @@ export class IndexPatternManagementService { this.indexPatternListConfig = new IndexPatternListManager(); } - public setup({ httpClient }: SetupDependencies) { + public setup({ httpClient, uiSettings }: SetupDependencies) { const creationManagerSetup = this.indexPatternCreationManager.setup(httpClient); creationManagerSetup.addCreationConfig(IndexPatternCreationConfig); const indexPatternListConfigSetup = this.indexPatternListConfig.setup(); indexPatternListConfigSetup.addListConfig(IndexPatternListConfig); - return { - creation: creationManagerSetup, - list: indexPatternListConfigSetup, - }; + if (uiSettings.get(CONFIG_ROLLUPS)) { + creationManagerSetup.addCreationConfig(RollupIndexPatternCreationConfig); + indexPatternListConfigSetup.addListConfig(RollupIndexPatternListConfig); + } } public start() { diff --git a/src/plugins/index_pattern_management/public/service/list/index.ts b/src/plugins/index_pattern_management/public/service/list/index.ts index 620d4c7600733b1..738b807ac762466 100644 --- a/src/plugins/index_pattern_management/public/service/list/index.ts +++ b/src/plugins/index_pattern_management/public/service/list/index.ts @@ -8,3 +8,5 @@ export { IndexPatternListConfig } from './config'; export { IndexPatternListManager } from './manager'; +// @ts-ignore +export { RollupIndexPatternListConfig } from './rollup_list_config'; diff --git a/x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js b/src/plugins/index_pattern_management/public/service/list/rollup_list_config.js similarity index 86% rename from x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js rename to src/plugins/index_pattern_management/public/service/list/rollup_list_config.js index 43eee6ca27f9a01..9a80d5fd0d622b9 100644 --- a/x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js +++ b/src/plugins/index_pattern_management/public/service/list/rollup_list_config.js @@ -1,11 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import { IndexPatternListConfig } from '../../../../../src/plugins/index_pattern_management/public'; +import { IndexPatternListConfig } from '.'; function isRollup(indexPattern) { return ( diff --git a/src/plugins/newsfeed/kibana.json b/src/plugins/newsfeed/kibana.json index b9f37b67f6921b5..0e7ae7cd11c35fb 100644 --- a/src/plugins/newsfeed/kibana.json +++ b/src/plugins/newsfeed/kibana.json @@ -2,5 +2,6 @@ "id": "newsfeed", "version": "kibana", "server": true, - "ui": true + "ui": true, + "requiredPlugins": ["screenshotMode"] } diff --git a/src/plugins/newsfeed/public/lib/api.test.mocks.ts b/src/plugins/newsfeed/public/lib/api.test.mocks.ts index 677bc203cbef3ff..8ac66eae6c2f6c9 100644 --- a/src/plugins/newsfeed/public/lib/api.test.mocks.ts +++ b/src/plugins/newsfeed/public/lib/api.test.mocks.ts @@ -8,6 +8,7 @@ import { storageMock } from './storage.mock'; import { driverMock } from './driver.mock'; +import { NeverFetchNewsfeedApiDriver } from './never_fetch_driver'; export const storageInstanceMock = storageMock.create(); jest.doMock('./storage', () => ({ @@ -18,3 +19,7 @@ export const driverInstanceMock = driverMock.create(); jest.doMock('./driver', () => ({ NewsfeedApiDriver: jest.fn().mockImplementation(() => driverInstanceMock), })); + +jest.doMock('./never_fetch_driver', () => ({ + NeverFetchNewsfeedApiDriver: jest.fn(() => new NeverFetchNewsfeedApiDriver()), +})); diff --git a/src/plugins/newsfeed/public/lib/api.test.ts b/src/plugins/newsfeed/public/lib/api.test.ts index a4894573932e6c6..58d06e72cd77c4a 100644 --- a/src/plugins/newsfeed/public/lib/api.test.ts +++ b/src/plugins/newsfeed/public/lib/api.test.ts @@ -7,12 +7,16 @@ */ import { driverInstanceMock, storageInstanceMock } from './api.test.mocks'; + import moment from 'moment'; import { getApi } from './api'; import { TestScheduler } from 'rxjs/testing'; import { FetchResult, NewsfeedPluginBrowserConfig } from '../types'; import { take } from 'rxjs/operators'; +import { NewsfeedApiDriver as MockNewsfeedApiDriver } from './driver'; +import { NeverFetchNewsfeedApiDriver as MockNeverFetchNewsfeedApiDriver } from './never_fetch_driver'; + const kibanaVersion = '8.0.0'; const newsfeedId = 'test'; @@ -46,6 +50,8 @@ describe('getApi', () => { afterEach(() => { storageInstanceMock.isAnyUnread$.mockReset(); driverInstanceMock.fetchNewsfeedItems.mockReset(); + (MockNewsfeedApiDriver as jest.Mock).mockClear(); + (MockNeverFetchNewsfeedApiDriver as jest.Mock).mockClear(); }); it('merges the newsfeed and unread observables', () => { @@ -60,7 +66,7 @@ describe('getApi', () => { a: createFetchResult({ feedItems: ['item' as any] }), }) ); - const api = getApi(createConfig(1000), kibanaVersion, newsfeedId); + const api = getApi(createConfig(1000), kibanaVersion, newsfeedId, false); expectObservable(api.fetchResults$.pipe(take(1))).toBe('(a|)', { a: createFetchResult({ @@ -83,7 +89,7 @@ describe('getApi', () => { a: createFetchResult({ feedItems: ['item' as any] }), }) ); - const api = getApi(createConfig(2), kibanaVersion, newsfeedId); + const api = getApi(createConfig(2), kibanaVersion, newsfeedId, false); expectObservable(api.fetchResults$.pipe(take(2))).toBe('a-(b|)', { a: createFetchResult({ @@ -111,7 +117,7 @@ describe('getApi', () => { a: createFetchResult({}), }) ); - const api = getApi(createConfig(10), kibanaVersion, newsfeedId); + const api = getApi(createConfig(10), kibanaVersion, newsfeedId, false); expectObservable(api.fetchResults$.pipe(take(2))).toBe('a--(b|)', { a: createFetchResult({ @@ -123,4 +129,16 @@ describe('getApi', () => { }); }); }); + + it('uses the news feed API driver if in not screenshot mode', () => { + getApi(createConfig(10), kibanaVersion, newsfeedId, false); + expect(MockNewsfeedApiDriver).toHaveBeenCalled(); + expect(MockNeverFetchNewsfeedApiDriver).not.toHaveBeenCalled(); + }); + + it('uses the never fetch news feed API driver if in not screenshot mode', () => { + getApi(createConfig(10), kibanaVersion, newsfeedId, true); + expect(MockNewsfeedApiDriver).not.toHaveBeenCalled(); + expect(MockNeverFetchNewsfeedApiDriver).toHaveBeenCalled(); + }); }); diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts index 4fbbd8687b73fcd..7aafc9fd2762502 100644 --- a/src/plugins/newsfeed/public/lib/api.ts +++ b/src/plugins/newsfeed/public/lib/api.ts @@ -11,6 +11,7 @@ import { map, catchError, filter, mergeMap, tap } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { FetchResult, NewsfeedPluginBrowserConfig } from '../types'; import { NewsfeedApiDriver } from './driver'; +import { NeverFetchNewsfeedApiDriver } from './never_fetch_driver'; import { NewsfeedStorage } from './storage'; export enum NewsfeedApiEndpoint { @@ -40,13 +41,23 @@ export interface NewsfeedApi { export function getApi( config: NewsfeedPluginBrowserConfig, kibanaVersion: string, - newsfeedId: string + newsfeedId: string, + isScreenshotMode: boolean ): NewsfeedApi { - const userLanguage = i18n.getLocale(); - const fetchInterval = config.fetchInterval.asMilliseconds(); - const mainInterval = config.mainInterval.asMilliseconds(); const storage = new NewsfeedStorage(newsfeedId); - const driver = new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval, storage); + const mainInterval = config.mainInterval.asMilliseconds(); + + const createNewsfeedApiDriver = () => { + if (isScreenshotMode) { + return new NeverFetchNewsfeedApiDriver(); + } + + const userLanguage = i18n.getLocale(); + const fetchInterval = config.fetchInterval.asMilliseconds(); + return new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval, storage); + }; + + const driver = createNewsfeedApiDriver(); const results$ = timer(0, mainInterval).pipe( filter(() => driver.shouldFetch()), diff --git a/src/plugins/newsfeed/public/lib/driver.ts b/src/plugins/newsfeed/public/lib/driver.ts index 0efa981e8c89d67..1762c4a42878440 100644 --- a/src/plugins/newsfeed/public/lib/driver.ts +++ b/src/plugins/newsfeed/public/lib/driver.ts @@ -10,6 +10,7 @@ import moment from 'moment'; import * as Rx from 'rxjs'; import { NEWSFEED_DEFAULT_SERVICE_BASE_URL } from '../../common/constants'; import { ApiItem, FetchResult, NewsfeedPluginBrowserConfig } from '../types'; +import { INewsfeedApiDriver } from './types'; import { convertItems } from './convert_items'; import type { NewsfeedStorage } from './storage'; @@ -19,7 +20,7 @@ interface NewsfeedResponse { items: ApiItem[]; } -export class NewsfeedApiDriver { +export class NewsfeedApiDriver implements INewsfeedApiDriver { private readonly kibanaVersion: string; private readonly loadedTime = moment().utc(); // the date is compared to time in UTC format coming from the service diff --git a/src/plugins/newsfeed/public/lib/never_fetch_driver.ts b/src/plugins/newsfeed/public/lib/never_fetch_driver.ts new file mode 100644 index 000000000000000..e95ca9c2d499a76 --- /dev/null +++ b/src/plugins/newsfeed/public/lib/never_fetch_driver.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Observable } from 'rxjs'; +import { FetchResult } from '../types'; +import { INewsfeedApiDriver } from './types'; + +/** + * NewsfeedApiDriver variant that never fetches results. This is useful for instances where Kibana is started + * without any user interaction like when generating a PDF or PNG report. + */ +export class NeverFetchNewsfeedApiDriver implements INewsfeedApiDriver { + shouldFetch(): boolean { + return false; + } + + fetchNewsfeedItems(): Observable { + throw new Error('Not implemented!'); + } +} diff --git a/src/plugins/newsfeed/public/lib/types.ts b/src/plugins/newsfeed/public/lib/types.ts new file mode 100644 index 000000000000000..5a62a929eeb7ff4 --- /dev/null +++ b/src/plugins/newsfeed/public/lib/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Observable } from 'rxjs'; +import type { FetchResult, NewsfeedPluginBrowserConfig } from '../types'; + +export interface INewsfeedApiDriver { + /** + * Check whether newsfeed items should be (re-)fetched + */ + shouldFetch(): boolean; + + fetchNewsfeedItems(config: NewsfeedPluginBrowserConfig['service']): Observable; +} diff --git a/src/plugins/newsfeed/public/plugin.test.ts b/src/plugins/newsfeed/public/plugin.test.ts new file mode 100644 index 000000000000000..4be69feb79f5557 --- /dev/null +++ b/src/plugins/newsfeed/public/plugin.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { take } from 'rxjs/operators'; +import { coreMock } from '../../../core/public/mocks'; +import { NewsfeedPublicPlugin } from './plugin'; +import { NewsfeedApiEndpoint } from './lib/api'; + +describe('Newsfeed plugin', () => { + let plugin: NewsfeedPublicPlugin; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + plugin = new NewsfeedPublicPlugin(coreMock.createPluginInitializerContext()); + }); + + describe('#start', () => { + beforeEach(() => { + plugin.setup(coreMock.createSetup()); + }); + + beforeEach(() => { + /** + * We assume for these tests that the newsfeed stream exposed by start will fetch newsfeed items + * on the first tick for new subscribers + */ + jest.spyOn(window, 'fetch'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('base case', () => { + it('makes fetch requests', () => { + const startContract = plugin.start(coreMock.createStart(), { + screenshotMode: { isScreenshotMode: () => false }, + }); + const sub = startContract + .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do + .pipe(take(1)) + .subscribe(() => {}); + jest.runOnlyPendingTimers(); + expect(window.fetch).toHaveBeenCalled(); + sub.unsubscribe(); + }); + }); + + describe('when in screenshot mode', () => { + it('makes no fetch requests in screenshot mode', () => { + const startContract = plugin.start(coreMock.createStart(), { + screenshotMode: { isScreenshotMode: () => true }, + }); + const sub = startContract + .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do + .pipe(take(1)) + .subscribe(() => {}); + jest.runOnlyPendingTimers(); + expect(window.fetch).not.toHaveBeenCalled(); + sub.unsubscribe(); + }); + }); + }); +}); diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx index fdda0a24b8bd568..656fc2ef00bb9f6 100644 --- a/src/plugins/newsfeed/public/plugin.tsx +++ b/src/plugins/newsfeed/public/plugin.tsx @@ -13,7 +13,7 @@ import React from 'react'; import moment from 'moment'; import { I18nProvider } from '@kbn/i18n/react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { NewsfeedPluginBrowserConfig } from './types'; +import { NewsfeedPluginBrowserConfig, NewsfeedPluginStartDependencies } from './types'; import { NewsfeedNavButton } from './components/newsfeed_header_nav_button'; import { getApi, NewsfeedApi, NewsfeedApiEndpoint } from './lib/api'; @@ -41,8 +41,10 @@ export class NewsfeedPublicPlugin return {}; } - public start(core: CoreStart) { - const api = this.createNewsfeedApi(this.config, NewsfeedApiEndpoint.KIBANA); + public start(core: CoreStart, { screenshotMode }: NewsfeedPluginStartDependencies) { + const isScreenshotMode = screenshotMode.isScreenshotMode(); + + const api = this.createNewsfeedApi(this.config, NewsfeedApiEndpoint.KIBANA, isScreenshotMode); core.chrome.navControls.registerRight({ order: 1000, mount: (target) => this.mount(api, target), @@ -56,7 +58,7 @@ export class NewsfeedPublicPlugin pathTemplate: `/${endpoint}/v{VERSION}.json`, }, }); - const { fetchResults$ } = this.createNewsfeedApi(config, endpoint); + const { fetchResults$ } = this.createNewsfeedApi(config, endpoint, isScreenshotMode); return fetchResults$; }, }; @@ -68,9 +70,10 @@ export class NewsfeedPublicPlugin private createNewsfeedApi( config: NewsfeedPluginBrowserConfig, - newsfeedId: NewsfeedApiEndpoint + newsfeedId: NewsfeedApiEndpoint, + isScreenshotMode: boolean ): NewsfeedApi { - const api = getApi(config, this.kibanaVersion, newsfeedId); + const api = getApi(config, this.kibanaVersion, newsfeedId, isScreenshotMode); return { markAsRead: api.markAsRead, fetchResults$: api.fetchResults$.pipe( diff --git a/src/plugins/newsfeed/public/types.ts b/src/plugins/newsfeed/public/types.ts index cca656565f4ca56..a7ff917f6f9750b 100644 --- a/src/plugins/newsfeed/public/types.ts +++ b/src/plugins/newsfeed/public/types.ts @@ -7,6 +7,10 @@ */ import { Duration, Moment } from 'moment'; +import type { ScreenshotModePluginStart } from 'src/plugins/screenshot_mode/public'; +export interface NewsfeedPluginStartDependencies { + screenshotMode: ScreenshotModePluginStart; +} // Ideally, we may want to obtain the type from the configSchema and exposeToBrowser keys... export interface NewsfeedPluginBrowserConfig { diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json index 66244a22336c777..18e6f2de1bc6fb2 100644 --- a/src/plugins/newsfeed/tsconfig.json +++ b/src/plugins/newsfeed/tsconfig.json @@ -7,13 +7,9 @@ "declaration": true, "declarationMap": true }, - "include": [ - "public/**/*", - "server/**/*", - "common/*", - "../../../typings/**/*" - ], + "include": ["public/**/*", "server/**/*", "common/*", "../../../typings/**/*"], "references": [ - { "path": "../../core/tsconfig.json" } + { "path": "../../core/tsconfig.json" }, + { "path": "../screenshot_mode/tsconfig.json" } ] } diff --git a/src/plugins/screenshot_mode/public/index.ts b/src/plugins/screenshot_mode/public/index.ts index a5ad37dd5b760d5..012f57e837f4166 100644 --- a/src/plugins/screenshot_mode/public/index.ts +++ b/src/plugins/screenshot_mode/public/index.ts @@ -18,4 +18,4 @@ export { KBN_SCREENSHOT_MODE_ENABLED_KEY, } from '../common'; -export { ScreenshotModePluginSetup } from './types'; +export { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types'; diff --git a/src/plugins/screenshot_mode/public/plugin.test.ts b/src/plugins/screenshot_mode/public/plugin.test.ts index 33ae5014668760f..f2c0970d0ff60cc 100644 --- a/src/plugins/screenshot_mode/public/plugin.test.ts +++ b/src/plugins/screenshot_mode/public/plugin.test.ts @@ -21,7 +21,7 @@ describe('Screenshot mode public', () => { setScreenshotModeDisabled(); }); - describe('setup contract', () => { + describe('public contract', () => { it('detects screenshot mode "true"', () => { setScreenshotModeEnabled(); const screenshotMode = plugin.setup(coreMock.createSetup()); @@ -34,10 +34,4 @@ describe('Screenshot mode public', () => { expect(screenshotMode.isScreenshotMode()).toBe(false); }); }); - - describe('start contract', () => { - it('returns nothing', () => { - expect(plugin.start(coreMock.createStart())).toBe(undefined); - }); - }); }); diff --git a/src/plugins/screenshot_mode/public/plugin.ts b/src/plugins/screenshot_mode/public/plugin.ts index 7a166566a0173bc..a005bb7c3d055dd 100644 --- a/src/plugins/screenshot_mode/public/plugin.ts +++ b/src/plugins/screenshot_mode/public/plugin.ts @@ -8,18 +8,22 @@ import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; -import { ScreenshotModePluginSetup } from './types'; +import { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types'; import { getScreenshotMode } from '../common'; export class ScreenshotModePlugin implements Plugin { + private publicContract = Object.freeze({ + isScreenshotMode: () => getScreenshotMode() === true, + }); + public setup(core: CoreSetup): ScreenshotModePluginSetup { - return { - isScreenshotMode: () => getScreenshotMode() === true, - }; + return this.publicContract; } - public start(core: CoreStart) {} + public start(core: CoreStart): ScreenshotModePluginStart { + return this.publicContract; + } public stop() {} } diff --git a/src/plugins/screenshot_mode/public/types.ts b/src/plugins/screenshot_mode/public/types.ts index 744ea8615f2a790..f6963de0cbd63f2 100644 --- a/src/plugins/screenshot_mode/public/types.ts +++ b/src/plugins/screenshot_mode/public/types.ts @@ -15,3 +15,4 @@ export interface IScreenshotModeService { } export type ScreenshotModePluginSetup = IScreenshotModeService; +export type ScreenshotModePluginStart = IScreenshotModeService; diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap index 17a91a4d43cc767..cbfece0b081c611 100644 --- a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap +++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap @@ -5,6 +5,7 @@ Object { "as": "tagloud_vis", "type": "render", "value": Object { + "syncColors": false, "visData": Object { "columns": Array [ Object { @@ -20,6 +21,12 @@ Object { "type": "datatable", }, "visParams": Object { + "bucket": Object { + "accessor": 1, + "format": Object { + "id": "number", + }, + }, "maxFontSize": 72, "metric": Object { "accessor": 0, @@ -29,6 +36,10 @@ Object { }, "minFontSize": 18, "orientation": "single", + "palette": Object { + "name": "default", + "type": "palette", + }, "scale": "linear", "showLabel": true, }, diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap index a8bc0b4c51678a3..fed6fb54288f27c 100644 --- a/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap @@ -84,6 +84,9 @@ Object { "orientation": Array [ "single", ], + "palette": Array [ + "default", + ], "scale": Array [ "linear", ], diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap deleted file mode 100644 index 88ed7c66a79a2b5..000000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`tag cloud tests tagcloudscreenshot should render simple image 1`] = `"foobarfoobar"`; diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap deleted file mode 100644 index d7707f64d8a4fce..000000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TagCloudVisualizationTest TagCloudVisualization - basics simple draw 1`] = `"CNINUSDEBR"`; - -exports[`TagCloudVisualizationTest TagCloudVisualization - basics with param change 1`] = `"CNINUSDEBR"`; - -exports[`TagCloudVisualizationTest TagCloudVisualization - basics with resize 1`] = `"CNINUSDEBR"`; diff --git a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js b/src/plugins/vis_type_tagcloud/public/components/feedback_message.js deleted file mode 100644 index 9e1d66b0a2faaea..000000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { Component, Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiIconTip } from '@elastic/eui'; - -export class FeedbackMessage extends Component { - constructor() { - super(); - this.state = { shouldShowTruncate: false, shouldShowIncomplete: false }; - } - - render() { - if (!this.state.shouldShowTruncate && !this.state.shouldShowIncomplete) { - return ''; - } - - return ( - - {this.state.shouldShowTruncate && ( -

- -

- )} - {this.state.shouldShowIncomplete && ( -

- -

- )} -
- } - /> - ); - } -} diff --git a/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx new file mode 100644 index 000000000000000..82663bbf7070cae --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { lazy } from 'react'; +import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; +import { TagCloudVisParams, TagCloudTypeProps } from '../types'; + +const TagCloudOptionsLazy = lazy(() => import('./tag_cloud_options')); + +export const getTagCloudOptions = ({ palettes }: TagCloudTypeProps) => ( + props: VisEditorOptionsProps +) => ; diff --git a/src/plugins/vis_type_tagcloud/public/components/label.js b/src/plugins/vis_type_tagcloud/public/components/label.js deleted file mode 100644 index 028a001cfbe634e..000000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/label.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { Component } from 'react'; - -export class Label extends Component { - constructor() { - super(); - this.state = { label: '', shouldShowLabel: true }; - } - - render() { - return ( -
- {this.state.label} -
- ); - } -} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js deleted file mode 100644 index 254d210eebf3767..000000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import d3 from 'd3'; -import d3TagCloud from 'd3-cloud'; -import { EventEmitter } from 'events'; - -const ORIENTATIONS = { - single: () => 0, - 'right angled': (tag) => { - return hashWithinRange(tag.text, 2) * 90; - }, - multiple: (tag) => { - return hashWithinRange(tag.text, 12) * 15 - 90; //fan out 12 * 15 degrees over top-right and bottom-right quadrant (=-90 deg offset) - }, -}; -const D3_SCALING_FUNCTIONS = { - linear: () => d3.scale.linear(), - log: () => d3.scale.log(), - 'square root': () => d3.scale.sqrt(), -}; - -export class TagCloud extends EventEmitter { - constructor(domNode, colorScale) { - super(); - - //DOM - this._element = domNode; - this._d3SvgContainer = d3.select(this._element).append('svg'); - this._svgGroup = this._d3SvgContainer.append('g'); - this._size = [1, 1]; - this.resize(); - - //SETTING (non-configurable) - /** - * the fontFamily should be set explicitly for calculating a layout - * and to avoid words overlapping - */ - this._fontFamily = 'Inter UI, sans-serif'; - this._fontStyle = 'normal'; - this._fontWeight = 'normal'; - this._spiral = 'archimedean'; //layout shape - this._timeInterval = 1000; //time allowed for layout algorithm - this._padding = 5; - - //OPTIONS - this._orientation = 'single'; - this._minFontSize = 10; - this._maxFontSize = 36; - this._textScale = 'linear'; - this._optionsAsString = null; - - //DATA - this._words = null; - - //UTIL - this._colorScale = colorScale; - this._setTimeoutId = null; - this._pendingJob = null; - this._layoutIsUpdating = null; - this._allInViewBox = false; - this._DOMisUpdating = false; - } - - setOptions(options) { - if (JSON.stringify(options) === this._optionsAsString) { - return; - } - this._optionsAsString = JSON.stringify(options); - this._orientation = options.orientation; - this._minFontSize = Math.min(options.minFontSize, options.maxFontSize); - this._maxFontSize = Math.max(options.minFontSize, options.maxFontSize); - this._textScale = options.scale; - this._invalidate(false); - } - - resize() { - const newWidth = this._element.offsetWidth; - const newHeight = this._element.offsetHeight; - - if (newWidth === this._size[0] && newHeight === this._size[1]) { - return; - } - - const wasInside = this._size[0] >= this._cloudWidth && this._size[1] >= this._cloudHeight; - const willBeInside = this._cloudWidth <= newWidth && this._cloudHeight <= newHeight; - this._size[0] = newWidth; - this._size[1] = newHeight; - if (wasInside && willBeInside && this._allInViewBox) { - this._invalidate(true); - } else { - this._invalidate(false); - } - } - - setData(data) { - this._words = data; - this._invalidate(false); - } - - destroy() { - clearTimeout(this._setTimeoutId); - this._element.innerHTML = ''; - } - - getStatus() { - return this._allInViewBox ? TagCloud.STATUS.COMPLETE : TagCloud.STATUS.INCOMPLETE; - } - - _updateContainerSize() { - this._d3SvgContainer.attr('width', this._size[0]); - this._d3SvgContainer.attr('height', this._size[1]); - this._svgGroup.attr('width', this._size[0]); - this._svgGroup.attr('height', this._size[1]); - } - - _isJobRunning() { - return this._setTimeoutId || this._layoutIsUpdating || this._DOMisUpdating; - } - - async _processPendingJob() { - if (!this._pendingJob) { - return; - } - - if (this._isJobRunning()) { - return; - } - - this._completedJob = null; - const job = await this._pickPendingJob(); - if (job.words.length) { - if (job.refreshLayout) { - await this._updateLayout(job); - } - await this._updateDOM(job); - const cloudBBox = this._svgGroup[0][0].getBBox(); - this._cloudWidth = cloudBBox.width; - this._cloudHeight = cloudBBox.height; - this._allInViewBox = - cloudBBox.x >= 0 && - cloudBBox.y >= 0 && - cloudBBox.x + cloudBBox.width <= this._element.offsetWidth && - cloudBBox.y + cloudBBox.height <= this._element.offsetHeight; - } else { - this._emptyDOM(job); - } - - if (this._pendingJob) { - this._processPendingJob(); //pick up next job - } else { - this._completedJob = job; - this.emit('renderComplete'); - } - } - - async _pickPendingJob() { - return await new Promise((resolve) => { - this._setTimeoutId = setTimeout(async () => { - const job = this._pendingJob; - this._pendingJob = null; - this._setTimeoutId = null; - resolve(job); - }, 0); - }); - } - - _emptyDOM() { - this._svgGroup.selectAll('text').remove(); - this._cloudWidth = 0; - this._cloudHeight = 0; - this._allInViewBox = true; - this._DOMisUpdating = false; - } - - async _updateDOM(job) { - const canSkipDomUpdate = this._pendingJob || this._setTimeoutId; - if (canSkipDomUpdate) { - this._DOMisUpdating = false; - return; - } - - this._DOMisUpdating = true; - const affineTransform = positionWord.bind( - null, - this._element.offsetWidth / 2, - this._element.offsetHeight / 2 - ); - const svgTextNodes = this._svgGroup.selectAll('text'); - const stage = svgTextNodes.data(job.words, getText); - - await new Promise((resolve) => { - const enterSelection = stage.enter(); - const enteringTags = enterSelection.append('text'); - enteringTags.style('font-size', getSizeInPixels); - enteringTags.style('font-style', this._fontStyle); - enteringTags.style('font-weight', () => this._fontWeight); - enteringTags.style('font-family', () => this._fontFamily); - enteringTags.style('fill', this.getFill.bind(this)); - enteringTags.attr('text-anchor', () => 'middle'); - enteringTags.attr('transform', affineTransform); - enteringTags.attr('data-test-subj', getDisplayText); - enteringTags.text(getDisplayText); - - const self = this; - enteringTags.on({ - click: function (event) { - self.emit('select', event); - }, - mouseover: function () { - d3.select(this).style('cursor', 'pointer'); - }, - mouseout: function () { - d3.select(this).style('cursor', 'default'); - }, - }); - - const movingTags = stage.transition(); - movingTags.duration(600); - movingTags.style('font-size', getSizeInPixels); - movingTags.style('font-style', this._fontStyle); - movingTags.style('font-weight', () => this._fontWeight); - movingTags.style('font-family', () => this._fontFamily); - movingTags.attr('transform', affineTransform); - - const exitingTags = stage.exit(); - const exitTransition = exitingTags.transition(); - exitTransition.duration(200); - exitingTags.style('fill-opacity', 1e-6); - exitingTags.attr('font-size', 1); - exitingTags.remove(); - - let exits = 0; - let moves = 0; - const resolveWhenDone = () => { - if (exits === 0 && moves === 0) { - this._DOMisUpdating = false; - resolve(true); - } - }; - exitTransition.each(() => exits++); - exitTransition.each('end', () => { - exits--; - resolveWhenDone(); - }); - movingTags.each(() => moves++); - movingTags.each('end', () => { - moves--; - resolveWhenDone(); - }); - }); - } - - _makeTextSizeMapper() { - const mapSizeToFontSize = D3_SCALING_FUNCTIONS[this._textScale](); - const range = - this._words.length === 1 - ? [this._maxFontSize, this._maxFontSize] - : [this._minFontSize, this._maxFontSize]; - mapSizeToFontSize.range(range); - if (this._words) { - mapSizeToFontSize.domain(d3.extent(this._words, getValue)); - } - return mapSizeToFontSize; - } - - _makeNewJob() { - return { - refreshLayout: true, - size: this._size.slice(), - words: this._words, - }; - } - - _makeJobPreservingLayout() { - return { - refreshLayout: false, - size: this._size.slice(), - words: this._completedJob.words.map((tag) => { - return { - x: tag.x, - y: tag.y, - rotate: tag.rotate, - size: tag.size, - rawText: tag.rawText || tag.text, - displayText: tag.displayText, - meta: tag.meta, - }; - }), - }; - } - - _invalidate(keepLayout) { - if (!this._words) { - return; - } - - this._updateContainerSize(); - - const canReuseLayout = keepLayout && !this._isJobRunning() && this._completedJob; - this._pendingJob = canReuseLayout ? this._makeJobPreservingLayout() : this._makeNewJob(); - this._processPendingJob(); - } - - async _updateLayout(job) { - if (job.size[0] <= 0 || job.size[1] <= 0) { - // If either width or height isn't above 0 we don't relayout anything, - // since the d3-cloud will be stuck in an infinite loop otherwise. - return; - } - - const mapSizeToFontSize = this._makeTextSizeMapper(); - const tagCloudLayoutGenerator = d3TagCloud(); - tagCloudLayoutGenerator.size(job.size); - tagCloudLayoutGenerator.padding(this._padding); - tagCloudLayoutGenerator.rotate(ORIENTATIONS[this._orientation]); - tagCloudLayoutGenerator.font(this._fontFamily); - tagCloudLayoutGenerator.fontStyle(this._fontStyle); - tagCloudLayoutGenerator.fontWeight(this._fontWeight); - tagCloudLayoutGenerator.fontSize((tag) => mapSizeToFontSize(tag.value)); - tagCloudLayoutGenerator.random(seed); - tagCloudLayoutGenerator.spiral(this._spiral); - tagCloudLayoutGenerator.words(job.words); - tagCloudLayoutGenerator.text(getDisplayText); - tagCloudLayoutGenerator.timeInterval(this._timeInterval); - - this._layoutIsUpdating = true; - await new Promise((resolve) => { - tagCloudLayoutGenerator.on('end', () => { - this._layoutIsUpdating = false; - resolve(true); - }); - tagCloudLayoutGenerator.start(); - }); - } - - /** - * Returns debug info. For debugging only. - * @return {*} - */ - getDebugInfo() { - const debug = {}; - debug.positions = this._completedJob - ? this._completedJob.words.map((tag) => { - return { - displayText: tag.displayText, - rawText: tag.rawText || tag.text, - x: tag.x, - y: tag.y, - rotate: tag.rotate, - }; - }) - : []; - debug.size = { - width: this._size[0], - height: this._size[1], - }; - return debug; - } - - getFill(tag) { - return this._colorScale(tag.text); - } -} - -TagCloud.STATUS = { COMPLETE: 0, INCOMPLETE: 1 }; - -function seed() { - return 0.5; //constant seed (not random) to ensure constant layouts for identical data -} - -function getText(word) { - return word.rawText; -} - -function getDisplayText(word) { - return word.displayText; -} - -function positionWord(xTranslate, yTranslate, word) { - if (isNaN(word.x) || isNaN(word.y) || isNaN(word.rotate)) { - //move off-screen - return `translate(${xTranslate * 3}, ${yTranslate * 3})rotate(0)`; - } - - return `translate(${word.x + xTranslate}, ${word.y + yTranslate})rotate(${word.rotate})`; -} - -function getValue(tag) { - return tag.value; -} - -function getSizeInPixels(tag) { - return `${tag.size}px`; -} - -function hashWithinRange(str, max) { - str = JSON.stringify(str); - let hash = 0; - for (const ch of str) { - hash = (hash * 31 + ch.charCodeAt(0)) % max; - } - return Math.abs(hash) % max; -} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss index 37867f1ed1c178c..51b5e9dedd84420 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss @@ -5,18 +5,14 @@ // tgcChart__legend--small // tgcChart__legend-isLoading -.tgcChart__container, .tgcChart__wrapper { +.tgcChart__wrapper { flex: 1 1 0; display: flex; + flex-direction: column; } -.tgcChart { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - overflow: hidden; +.tgcChart__wrapper text { + cursor: pointer; } .tgcChart__label { @@ -24,3 +20,7 @@ text-align: center; font-weight: $euiFontWeightBold; } + +.tgcChart__warning { + width: $euiSize; +} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js deleted file mode 100644 index eb575457146c5d6..000000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js +++ /dev/null @@ -1,507 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import d3 from 'd3'; -import 'jest-canvas-mock'; - -import { fromNode, delay } from 'bluebird'; -import { TagCloud } from './tag_cloud'; -import { setHTMLElementOffset, setSVGElementGetBBox } from '@kbn/test/jest'; - -describe('tag cloud tests', () => { - let SVGElementGetBBoxSpyInstance; - let HTMLElementOffsetMockInstance; - - beforeEach(() => { - setupDOM(); - }); - - afterEach(() => { - SVGElementGetBBoxSpyInstance.mockRestore(); - HTMLElementOffsetMockInstance.mockRestore(); - }); - - const minValue = 1; - const maxValue = 9; - const midValue = (minValue + maxValue) / 2; - const baseTest = { - data: [ - { rawText: 'foo', displayText: 'foo', value: minValue }, - { rawText: 'bar', displayText: 'bar', value: midValue }, - { rawText: 'foobar', displayText: 'foobar', value: maxValue }, - ], - options: { - orientation: 'single', - scale: 'linear', - minFontSize: 10, - maxFontSize: 36, - }, - expected: [ - { - text: 'foo', - fontSize: '10px', - }, - { - text: 'bar', - fontSize: '23px', - }, - { - text: 'foobar', - fontSize: '36px', - }, - ], - }; - - const singleLayoutTest = _.cloneDeep(baseTest); - - const rightAngleLayoutTest = _.cloneDeep(baseTest); - rightAngleLayoutTest.options.orientation = 'right angled'; - - const multiLayoutTest = _.cloneDeep(baseTest); - multiLayoutTest.options.orientation = 'multiple'; - - const mapWithLog = d3.scale.log(); - mapWithLog.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]); - mapWithLog.domain([minValue, maxValue]); - const logScaleTest = _.cloneDeep(baseTest); - logScaleTest.options.scale = 'log'; - logScaleTest.expected[1].fontSize = Math.round(mapWithLog(midValue)) + 'px'; - - const mapWithSqrt = d3.scale.sqrt(); - mapWithSqrt.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]); - mapWithSqrt.domain([minValue, maxValue]); - const sqrtScaleTest = _.cloneDeep(baseTest); - sqrtScaleTest.options.scale = 'square root'; - sqrtScaleTest.expected[1].fontSize = Math.round(mapWithSqrt(midValue)) + 'px'; - - const biggerFontTest = _.cloneDeep(baseTest); - biggerFontTest.options.minFontSize = 36; - biggerFontTest.options.maxFontSize = 72; - biggerFontTest.expected[0].fontSize = '36px'; - biggerFontTest.expected[1].fontSize = '54px'; - biggerFontTest.expected[2].fontSize = '72px'; - - const trimDataTest = _.cloneDeep(baseTest); - trimDataTest.data.splice(1, 1); - trimDataTest.expected.splice(1, 1); - - let domNode; - let tagCloud; - - const colorScale = d3.scale - .ordinal() - .range(['#00a69b', '#57c17b', '#6f87d8', '#663db8', '#bc52bc', '#9e3533', '#daa05d']); - - function setupDOM() { - domNode = document.createElement('div'); - SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(); - HTMLElementOffsetMockInstance = setHTMLElementOffset(512, 512); - - document.body.appendChild(domNode); - } - - function teardownDOM() { - domNode.innerHTML = ''; - document.body.removeChild(domNode); - } - - [ - singleLayoutTest, - rightAngleLayoutTest, - multiLayoutTest, - logScaleTest, - sqrtScaleTest, - biggerFontTest, - trimDataTest, - ].forEach(function (currentTest) { - describe(`should position elements correctly for options: ${JSON.stringify( - currentTest.options - )}`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(currentTest.data); - tagCloud.setOptions(currentTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(currentTest.expected, textElements, tagCloud); - }) - ); - }); - }); - - [5, 100, 200, 300, 500].forEach((timeout) => { - // FLAKY: https://github.com/elastic/kibana/issues/94043 - describe.skip(`should only send single renderComplete event at the very end, using ${timeout}ms timeout`, () => { - beforeEach(async () => { - //TagCloud takes at least 600ms to complete (due to d3 animation) - //renderComplete should only notify at the last one - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - - //this timeout modifies the settings before the cloud is rendered. - //the cloud needs to use the correct options - setTimeout(() => tagCloud.setOptions(logScaleTest.options), timeout); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(logScaleTest.expected, textElements, tagCloud); - }) - ); - }); - }); - - describe('should use the latest state before notifying (when modifying options multiple times)', () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - tagCloud.setOptions(logScaleTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(logScaleTest.expected, textElements, tagCloud); - }) - ); - }); - - describe('should use the latest state before notifying (when modifying data multiple times)', () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - tagCloud.setData(trimDataTest.data); - - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(trimDataTest.expected, textElements, tagCloud); - }) - ); - }); - - describe('should not get multiple render-events', () => { - let counter; - beforeEach(() => { - counter = 0; - - return new Promise((resolve, reject) => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - - setTimeout(() => { - //this should be overridden by later changes - tagCloud.setData(sqrtScaleTest.data); - tagCloud.setOptions(sqrtScaleTest.options); - }, 100); - - setTimeout(() => { - //latest change - tagCloud.setData(logScaleTest.data); - tagCloud.setOptions(logScaleTest.options); - }, 300); - - tagCloud.on('renderComplete', function onRender() { - if (counter > 0) { - reject('Should not get multiple render events'); - } - counter += 1; - resolve(true); - }); - }); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(logScaleTest.expected, textElements, tagCloud); - }) - ); - }); - - describe('should show correct data when state-updates are interleaved with resize event', () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(logScaleTest.data); - tagCloud.setOptions(logScaleTest.options); - - await delay(1000); //let layout run - - SVGElementGetBBoxSpyInstance.mockRestore(); - SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(600, 600); - - tagCloud.resize(); //triggers new layout - setTimeout(() => { - //change the options at the very end too - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - }, 200); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(baseTest.expected, textElements, tagCloud); - }) - ); - }); - - describe(`should not put elements in view when container is too small`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test('completeness should not be ok', () => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE); - }); - test('positions should not be ok', () => { - const textElements = domNode.querySelectorAll('text'); - for (let i = 0; i < textElements; i++) { - const bbox = textElements[i].getBoundingClientRect(); - verifyBbox(bbox, false, tagCloud); - } - }); - }); - - describe(`tags should fit after making container bigger`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - - //make bigger - tagCloud._size = [600, 600]; - tagCloud.resize(); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - }); - - describe(`tags should no longer fit after making container smaller`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - - //make smaller - tagCloud._size = []; - tagCloud.resize(); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test('completeness should not be ok', () => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE); - }); - }); - - describe('tagcloudscreenshot', () => { - afterEach(teardownDOM); - - test('should render simple image', async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - - expect(domNode.innerHTML).toMatchSnapshot(); - }); - }); - - function verifyTagProperties(expectedValues, actualElements, tagCloud) { - expect(actualElements.length).toEqual(expectedValues.length); - expectedValues.forEach((test, index) => { - try { - expect(actualElements[index].style.fontSize).toEqual(test.fontSize); - } catch (e) { - throw new Error('fontsize is not correct: ' + e.message); - } - try { - expect(actualElements[index].innerHTML).toEqual(test.text); - } catch (e) { - throw new Error('fontsize is not correct: ' + e.message); - } - isInsideContainer(actualElements[index], tagCloud); - }); - } - - function isInsideContainer(actualElement, tagCloud) { - const bbox = actualElement.getBoundingClientRect(); - verifyBbox(bbox, true, tagCloud); - } - - function verifyBbox(bbox, shouldBeInside, tagCloud) { - const message = ` | bbox-of-tag: ${JSON.stringify([ - bbox.left, - bbox.top, - bbox.right, - bbox.bottom, - ])} vs - bbox-of-container: ${domNode.offsetWidth},${domNode.offsetHeight} - debugInfo: ${JSON.stringify(tagCloud.getDebugInfo())}`; - - try { - expect(bbox.top >= 0 && bbox.top <= domNode.offsetHeight).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'top boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message - ); - } - try { - expect(bbox.bottom >= 0 && bbox.bottom <= domNode.offsetHeight).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'bottom boundary of tag should have been ' + - (shouldBeInside ? 'inside' : 'outside') + - message - ); - } - try { - expect(bbox.left >= 0 && bbox.left <= domNode.offsetWidth).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'left boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message - ); - } - try { - expect(bbox.right >= 0 && bbox.right <= domNode.offsetWidth).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'right boundary of tag should have been ' + - (shouldBeInside ? 'inside' : 'outside') + - message - ); - } - } - - /** - * In CI, this entire suite "blips" about 1/5 times. - * This blip causes the majority of these tests fail for the exact same reason: One tag is centered inside the container, - * while the others are moved out. - * This has not been reproduced locally yet. - * It may be an issue with the 3rd party d3-cloud that snags. - * - * The test suite should continue to catch reliably catch regressions of other sorts: unexpected and other uncaught errors, - * scaling issues, ordering issues - * - */ - function shouldAssert() { - const debugInfo = tagCloud.getDebugInfo(); - const count = debugInfo.positions.length; - const largest = debugInfo.positions.pop(); //test suite puts largest tag at the end. - - const centered = largest[1] === 0 && largest[2] === 0; - const halfWidth = debugInfo.size.width / 2; - const halfHeight = debugInfo.size.height / 2; - const inside = debugInfo.positions.filter((position) => { - const x = position.x + halfWidth; - const y = position.y + halfHeight; - return 0 <= x && x <= debugInfo.size.width && 0 <= y && y <= debugInfo.size.height; - }); - - return centered && inside.length === count - 1; - } - - function handleExpectedBlip(assertion) { - return () => { - if (!shouldAssert()) { - return; - } - assertion(); - }; - } -}); diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx new file mode 100644 index 000000000000000..b4d4e70d5ffe3e2 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { Wordcloud, Settings } from '@elastic/charts'; +import { chartPluginMock } from '../../../charts/public/mocks'; +import type { Datatable } from '../../../expressions/public'; +import { mount } from 'enzyme'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import TagCloudChart, { TagCloudChartProps } from './tag_cloud_chart'; +import { TagCloudVisParams } from '../types'; + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => { + return { + deserialize: jest.fn(), + }; + }), +})); + +const palettesRegistry = chartPluginMock.createPaletteRegistry(); +const visData = ({ + columns: [ + { + id: 'col-0', + name: 'geo.dest: Descending', + }, + { + id: 'col-1', + name: 'Count', + }, + ], + rows: [ + { 'col-0': 'CN', 'col-1': 26 }, + { 'col-0': 'IN', 'col-1': 17 }, + { 'col-0': 'US', 'col-1': 6 }, + { 'col-0': 'DE', 'col-1': 4 }, + { 'col-0': 'BR', 'col-1': 3 }, + ], +} as unknown) as Datatable; + +const visParams = { + bucket: { accessor: 0, format: {} }, + metric: { accessor: 1, format: {} }, + scale: 'linear', + orientation: 'single', + palette: { + type: 'palette', + name: 'default', + }, + minFontSize: 12, + maxFontSize: 70, + showLabel: true, +} as TagCloudVisParams; + +describe('TagCloudChart', function () { + let wrapperProps: TagCloudChartProps; + + beforeAll(() => { + wrapperProps = { + visData, + visParams, + palettesRegistry, + fireEvent: jest.fn(), + renderComplete: jest.fn(), + syncColors: false, + visType: 'tagcloud', + }; + }); + + it('renders the Wordcloud component', async () => { + const component = mount(); + expect(component.find(Wordcloud).length).toBe(1); + }); + + it('renders the label correctly', async () => { + const component = mount(); + const label = findTestSubject(component, 'tagCloudLabel'); + expect(label.text()).toEqual('geo.dest: Descending - Count'); + }); + + it('not renders the label if showLabel setting is off', async () => { + const newVisParams = { ...visParams, showLabel: false }; + const newProps = { ...wrapperProps, visParams: newVisParams }; + const component = mount(); + const label = findTestSubject(component, 'tagCloudLabel'); + expect(label.length).toBe(0); + }); + + it('receives the data on the correct format', () => { + const component = mount(); + expect(component.find(Wordcloud).prop('data')).toStrictEqual([ + { + color: 'black', + text: 'CN', + weight: 1, + }, + { + color: 'black', + text: 'IN', + weight: 0.6086956521739131, + }, + { + color: 'black', + text: 'US', + weight: 0.13043478260869565, + }, + { + color: 'black', + text: 'DE', + weight: 0.043478260869565216, + }, + { + color: 'black', + text: 'BR', + weight: 0, + }, + ]); + }); + + it('sets the angles correctly', async () => { + const newVisParams = { ...visParams, orientation: 'right angled' } as TagCloudVisParams; + const newProps = { ...wrapperProps, visParams: newVisParams }; + const component = mount(); + expect(component.find(Wordcloud).prop('endAngle')).toBe(90); + expect(component.find(Wordcloud).prop('angleCount')).toBe(2); + }); + + it('calls filter callback', () => { + const component = mount(); + component.find(Settings).prop('onElementClick')!([ + [ + { + text: 'BR', + weight: 0.17391304347826086, + color: '#d36086', + }, + { + specId: 'tagCloud', + key: 'tagCloud', + }, + ], + ]); + expect(wrapperProps.fireEvent).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx index f668e22815b60f2..b89fe2fa90ede0a 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx @@ -6,64 +6,225 @@ * Side Public License, v 1. */ -import React, { useEffect, useMemo, useRef } from 'react'; -import { EuiResizeObserver } from '@elastic/eui'; +import React, { useCallback, useState, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { throttle } from 'lodash'; - -import { TagCloudVisDependencies } from '../plugin'; +import { EuiIconTip, EuiResizeObserver } from '@elastic/eui'; +import { Chart, Settings, Wordcloud, RenderChangeListener } from '@elastic/charts'; +import type { PaletteRegistry } from '../../../charts/public'; +import type { IInterpreterRenderHandlers } from '../../../expressions/public'; +import { getFormatService } from '../services'; import { TagCloudVisRenderValue } from '../tag_cloud_fn'; -// @ts-ignore -import { TagCloudVisualization } from './tag_cloud_visualization'; import './tag_cloud.scss'; -type TagCloudChartProps = TagCloudVisDependencies & - TagCloudVisRenderValue & { - fireEvent: (event: any) => void; - renderComplete: () => void; - }; +const MAX_TAG_COUNT = 200; + +export type TagCloudChartProps = TagCloudVisRenderValue & { + fireEvent: IInterpreterRenderHandlers['event']; + renderComplete: IInterpreterRenderHandlers['done']; + palettesRegistry: PaletteRegistry; +}; + +const calculateWeight = (value: number, x1: number, y1: number, x2: number, y2: number) => + ((value - x1) * (y2 - x2)) / (y1 - x1) + x2; + +const getColor = ( + palettes: PaletteRegistry, + activePalette: string, + text: string, + values: string[], + syncColors: boolean +) => { + return palettes?.get(activePalette).getCategoricalColor( + [ + { + name: text, + rankAtDepth: values.length ? values.findIndex((name) => name === text) : 0, + totalSeriesAtDepth: values.length || 1, + }, + ], + { + maxDepth: 1, + totalSeries: values.length || 1, + behindText: false, + syncColors, + } + ); +}; + +const ORIENTATIONS = { + single: { + endAngle: 0, + angleCount: 360, + }, + 'right angled': { + endAngle: 90, + angleCount: 2, + }, + multiple: { + endAngle: -90, + angleCount: 12, + }, +}; export const TagCloudChart = ({ - colors, visData, visParams, + palettesRegistry, fireEvent, renderComplete, + syncColors, }: TagCloudChartProps) => { - const chartDiv = useRef(null); - const visController = useRef(null); + const [warning, setWarning] = useState(false); + const { bucket, metric, scale, palette, showLabel, orientation } = visParams; + const bucketFormatter = bucket ? getFormatService().deserialize(bucket.format) : null; - useEffect(() => { - if (chartDiv.current) { - visController.current = new TagCloudVisualization(chartDiv.current, colors, fireEvent); - } - return () => { - visController.current.destroy(); - visController.current = null; - }; - }, [colors, fireEvent]); - - useEffect(() => { - if (visController.current) { - visController.current.render(visData, visParams).then(renderComplete); - } - }, [visData, visParams, renderComplete]); + const tagCloudData = useMemo(() => { + const tagColumn = bucket ? visData.columns[bucket.accessor].id : -1; + const metricColumn = visData.columns[metric.accessor]?.id; + + const metrics = visData.rows.map((row) => row[metricColumn]); + const values = bucket ? visData.rows.map((row) => row[tagColumn]) : []; + const maxValue = Math.max(...metrics); + const minValue = Math.min(...metrics); + + return visData.rows.map((row) => { + const tag = row[tagColumn] === undefined ? 'all' : row[tagColumn]; + return { + text: (bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag) as string, + weight: + tag === 'all' || visData.rows.length <= 1 + ? 1 + : calculateWeight(row[metricColumn], minValue, maxValue, 0, 1) || 0, + color: getColor(palettesRegistry, palette.name, tag, values, syncColors) || 'rgba(0,0,0,0)', + }; + }); + }, [ + bucket, + bucketFormatter, + metric.accessor, + palette.name, + palettesRegistry, + syncColors, + visData.columns, + visData.rows, + ]); + + const label = bucket + ? `${visData.columns[bucket.accessor].name} - ${visData.columns[metric.accessor].name}` + : ''; + + const onRenderChange = useCallback( + (isRendered) => { + if (isRendered) { + renderComplete(); + } + }, + [renderComplete] + ); - const updateChartSize = useMemo( + const updateChart = useMemo( () => throttle(() => { - if (visController.current) { - visController.current.render(visData, visParams).then(renderComplete); - } + setWarning(false); }, 300), - [renderComplete, visData, visParams] + [] + ); + + const handleWordClick = useCallback( + (d) => { + if (!bucket) { + return; + } + const termsBucket = visData.columns[bucket.accessor]; + const clickedValue = d[0][0].text; + + const rowIndex = visData.rows.findIndex((row) => { + const formattedValue = bucketFormatter + ? bucketFormatter.convert(row[termsBucket.id], 'text') + : row[termsBucket.id]; + return formattedValue === clickedValue; + }); + + if (rowIndex < 0) { + return; + } + + fireEvent({ + name: 'filterBucket', + data: { + data: [ + { + table: visData, + column: bucket.accessor, + row: rowIndex, + }, + ], + }, + }); + }, + [bucket, bucketFormatter, fireEvent, visData] ); return ( - + {(resizeRef) => ( -
-
+
+ + + { + setWarning(true); + }} + /> + + {label && showLabel && ( +
+ {label} +
+ )} + {warning && ( +
+ + } + /> +
+ )} + {tagCloudData.length > MAX_TAG_COUNT && ( +
+ + } + /> +
+ )}
)} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index d5e005a63868060..6682799a8038adc 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -6,16 +6,22 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { SelectOption, SwitchOption } from '../../../vis_default_editor/public'; +import type { PaletteRegistry } from '../../../charts/public'; +import { VisEditorOptionsProps } from '../../../visualizations/public'; +import { SelectOption, SwitchOption, PalettePicker } from '../../../vis_default_editor/public'; import { ValidatedDualRange } from '../../../kibana_react/public'; -import { TagCloudVisParams } from '../types'; +import { TagCloudVisParams, TagCloudTypeProps } from '../types'; import { collections } from './collections'; -function TagCloudOptions({ stateParams, setValue }: VisEditorOptionsProps) { +interface TagCloudOptionsProps + extends VisEditorOptionsProps, + TagCloudTypeProps {} + +function TagCloudOptions({ stateParams, setValue, palettes }: TagCloudOptionsProps) { + const [palettesRegistry, setPalettesRegistry] = useState(undefined); const handleFontSizeChange = ([minFontSize, maxFontSize]: [string | number, string | number]) => { setValue('minFontSize', Number(minFontSize)); setValue('maxFontSize', Number(maxFontSize)); @@ -24,6 +30,14 @@ function TagCloudOptions({ stateParams, setValue }: VisEditorOptionsProps { + const fetchPalettes = async () => { + const palettesService = await palettes?.getPalettes(); + setPalettesRegistry(palettesService); + }; + fetchPalettes(); + }, [palettes]); + return ( + {palettesRegistry && ( + { + setValue(paramName, value); + }} + /> + )} + { - if (!this._visParams.bucket) { - return; - } - - fireEvent({ - name: 'filterBucket', - data: { - data: [ - { - table: event.meta.data, - column: 0, - row: event.meta.rowIndex, - }, - ], - }, - }); - }); - this._renderComplete$ = Rx.fromEvent(this._tagCloud, 'renderComplete'); - - this._feedbackNode = document.createElement('div'); - this._containerNode.appendChild(this._feedbackNode); - this._feedbackMessage = React.createRef(); - render( - - - , - this._feedbackNode - ); - - this._labelNode = document.createElement('div'); - this._containerNode.appendChild(this._labelNode); - this._label = React.createRef(); - render(