diff --git a/docs/infrastructure/images/infra-sysmon.png b/docs/infrastructure/images/infra-sysmon.png index 5b82d8c9b4e19..dd653bb046f45 100644 Binary files a/docs/infrastructure/images/infra-sysmon.png and b/docs/infrastructure/images/infra-sysmon.png differ diff --git a/docs/infrastructure/images/infra-view-metrics.png b/docs/infrastructure/images/infra-view-metrics.png index 9ad862ec6515d..6001f18d283fe 100644 Binary files a/docs/infrastructure/images/infra-view-metrics.png and b/docs/infrastructure/images/infra-view-metrics.png differ diff --git a/docs/infrastructure/images/metrics-add-data.png b/docs/infrastructure/images/metrics-add-data.png index d9640e0d9f5da..f96c30f0e1848 100644 Binary files a/docs/infrastructure/images/metrics-add-data.png and b/docs/infrastructure/images/metrics-add-data.png differ diff --git a/docs/infrastructure/images/metrics-explorer-screen.png b/docs/infrastructure/images/metrics-explorer-screen.png index 7ccf8891678af..6d56491f7d485 100644 Binary files a/docs/infrastructure/images/metrics-explorer-screen.png and b/docs/infrastructure/images/metrics-explorer-screen.png differ diff --git a/docs/logs/images/log-rate-anomalies.png b/docs/logs/images/log-rate-anomalies.png index ac9ff7c9a5235..74ce8d682e1cc 100644 Binary files a/docs/logs/images/log-rate-anomalies.png and b/docs/logs/images/log-rate-anomalies.png differ diff --git a/docs/logs/images/log-rate-entries.png b/docs/logs/images/log-rate-entries.png index f8a3acc9883e0..efa693a2ac529 100644 Binary files a/docs/logs/images/log-rate-entries.png and b/docs/logs/images/log-rate-entries.png differ diff --git a/docs/logs/images/log-time-filter.png b/docs/logs/images/log-time-filter.png index 863e488e6c6c0..ffba6f972aeb7 100644 Binary files a/docs/logs/images/log-time-filter.png and b/docs/logs/images/log-time-filter.png differ diff --git a/docs/logs/images/logs-add-data.png b/docs/logs/images/logs-add-data.png index 2c4a65590aa1b..176c71466aa38 100644 Binary files a/docs/logs/images/logs-add-data.png and b/docs/logs/images/logs-add-data.png differ diff --git a/docs/logs/images/logs-console.png b/docs/logs/images/logs-console.png index 5feb3d9608974..8e94c31c6862a 100644 Binary files a/docs/logs/images/logs-console.png and b/docs/logs/images/logs-console.png differ diff --git a/docs/logs/using.asciidoc b/docs/logs/using.asciidoc index f191f7d746cf8..d84a9260521c7 100644 --- a/docs/logs/using.asciidoc +++ b/docs/logs/using.asciidoc @@ -8,7 +8,6 @@ You can also view related application traces or uptime information where availab [role="screenshot"] image::logs/images/logs-console.png[Logs Console in Kibana] -// ++ Update this [float] [[logs-search]] diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index c942bddc9fd57..fa0edd8faadd7 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -56,6 +56,7 @@ - [On the server side](#on-the-server-side) - [On the client side](#on-the-client-side) - [Updates an application navlink at runtime](#updates-an-app-navlink-at-runtime) + - [Logging config migration](#logging-config-migration) Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now. @@ -1655,4 +1656,7 @@ export class MyPlugin implements Plugin { tooltip: 'Application disabled', }) } -``` \ No newline at end of file +``` + +### Logging config migration +[Read](./server/logging/README.md#logging-config-migration) \ No newline at end of file diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.md index 65fe64b045801..3fbec7a45148d 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -1,4 +1,12 @@ # Logging +- [Loggers, Appenders and Layouts](#loggers-appenders-and-layouts) +- [Logger hierarchy](#logger-hierarchy) +- [Log level](#log-level) +- [Layouts](#layouts) + - [Pattern layout](#pattern-layout) + - [JSON layout](#json-layout) +- [Configuration](#configuration) +- [Usage](#usage) The way logging works in Kibana is inspired by `log4j 2` logging framework used by [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html#logging). The main idea is to have consistent logging behaviour (configuration, log format etc.) across the entire Elastic Stack @@ -52,12 +60,68 @@ custom appenders, so one should always make the choice explicitly. There are two types of layout supported at the moment: `pattern` and `json`. -With `pattern` layout it's possible to define a string pattern with special placeholders wrapped into curly braces that +### Pattern layout +With `pattern` layout it's possible to define a string pattern with special placeholders `%conversion_pattern` (see the table below) that will be replaced with data from the actual log message. By default the following pattern is used: -`[{timestamp}][{level}][{context}] {message}`. Also `highlight` option can be enabled for `pattern` layout so that +`[%date][%level][%logger]%meta %message`. Also `highlight` option can be enabled for `pattern` layout so that some parts of the log message are highlighted with different colors that may be quite handy if log messages are forwarded to the terminal with color support. +`pattern` layout uses a sub-set of [log4j2 pattern syntax](https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout) +and **doesn't implement** all `log4j2` capabilities. The conversions that are provided out of the box are: +#### level +Outputs the [level](#log-level) of the logging event. +Example of `%level` output: +```bash +TRACE +DEBUG +INFO +``` + +##### logger +Outputs the name of the logger that published the logging event. +Example of `%logger` output: +```bash +server +server.http +server.http.Kibana +``` + +#### message +Outputs the application supplied message associated with the logging event. + +#### meta +Outputs the entries of `meta` object data in **json** format, if one is present in the event. +Example of `%meta` output: +```bash +// Meta{from: 'v7', to: 'v8'} +'{"from":"v7","to":"v8"}' +// Meta empty object +'{}' +// no Meta provided +'' +``` + +##### date +Outputs the date of the logging event. The date conversion specifier may be followed by a set of braces containing a name of predefined date format and canonical timezone name. +Timezone name is expected to be one from [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) +Example of `%date` output: + +| Conversion pattern | Example | +| ---------------------------------------- | ---------------------------------------------------------------- | +| `%date` | `2012-02-01T14:30:22.011Z` uses `ISO8601` format by default | +| `%date{ISO8601}` | `2012-02-01T14:30:22.011Z` | +| `%date{ISO8601_TZ}` | `2012-02-01T09:30:22.011-05:00` `ISO8601` with timezone | +| `%date{ISO8601_TZ}{America/Los_Angeles}` | `2012-02-01T06:30:22.011-08:00` | +| `%date{ABSOLUTE}` | `09:30:22.011` | +| `%date{ABSOLUTE}{America/Los_Angeles}` | `06:30:22.011` | +| `%date{UNIX}` | `1328106622` | +| `%date{UNIX_MILLIS}` | `1328106622011` | + +#### pid +Outputs the process ID. + +### JSON layout With `json` layout log messages will be formatted as JSON strings that include timestamp, log level, context, message text and any other metadata that may be associated with the log message itself. @@ -88,7 +152,7 @@ logging: kind: console layout: kind: pattern - pattern: [{timestamp}][{level}] {message} + pattern: "[%date][%level] %message" json-file-appender: kind: file path: /var/log/kibana-json.log @@ -179,3 +243,81 @@ The log will be less verbose with `warn` level for the `server` context: [2017-07-25T18:54:41.639Z][ERROR][server] Message with `error` log level. [2017-07-25T18:54:41.639Z][FATAL][server] Message with `fatal` log level. ``` + +### Logging config migration +Compatibility with the legacy logging system is assured until the end of the `v7` version. +All log messages handled by `root` context are forwarded to the legacy logging service. If you re-write +root appenders, make sure that it contains `default` appender to provide backward compatibility. +**Note**: If you define an appender for a context, the log messages aren't handled by the +`root` context anymore and not forwarded to the legacy logging service. + +#### logging.dest +By default logs in *stdout*. With new Kibana logging you can use pre-existing `console` appender or +define a custom one. +```yaml +logging: + loggers: + - context: your-plugin + appenders: [console] +``` +Logs in a *file* if given file path. You should define a custom appender with `kind: file` +```yaml + +logging: + appenders: + file: + kind: file + path: /var/log/kibana.log + layout: + kind: pattern + loggers: + - context: your-plugin + appenders: [file] +``` +#### logging.json +Defines the format of log output. Logs in JSON if `true`. With new logging config you can adjust +the output format with [layouts](#layouts). + +#### logging.quiet +Suppresses all logging output other than error messages. With new logging, config can be achieved +with adjusting minimum required [logging level](#log-level) +```yaml + loggers: + - context: my-plugin + appenders: [console] + level: error +# or for all output +logging.root.level: error +``` + +#### logging.silent: +Suppresses all logging output. +```yaml +logging.root.level: off +``` + +#### logging.verbose: +Logs all events +```yaml +logging.root.level: all +``` + +#### logging.timezone +Set to the canonical timezone id to log events using that timezone. New logging config allows +to [specify timezone](#date) for `layout: pattern`. +```yaml +logging: + appenders: + custom-console: + kind: console + layout: + kind: pattern + highlight: true + pattern: "[%level] [%date{ISO8601_TZ}{America/Los_Angeles}][%logger] %message" +``` + +#### logging.events +Define a custom logger for a specific context. + +#### logging.filter +TBD diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index 7142f91300f12..b88f5ba2c2b60 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -29,7 +29,7 @@ function createRoot() { layout: { highlight: false, kind: 'pattern', - pattern: '{level}|{context}|{message}', + pattern: '%level|%logger|%message', }, }, }, diff --git a/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap index da57023c94286..14c071b40ad7a 100644 --- a/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap +++ b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap @@ -1,9 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`\`format()\` correctly formats error record with meta-data 1`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-with-meta\\",\\"level\\":\\"DEBUG\\",\\"message\\":\\"message-with-meta\\",\\"meta\\":{\\"from\\":\\"v7\\",\\"to\\":\\"v8\\"},\\"pid\\":5355}"`; - -exports[`\`format()\` correctly formats record with meta-data 1`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-with-meta\\",\\"level\\":\\"DEBUG\\",\\"message\\":\\"message-with-meta\\",\\"meta\\":{\\"from\\":\\"v7\\",\\"to\\":\\"v8\\"},\\"pid\\":5355}"`; - exports[`\`format()\` correctly formats record. 1`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-1\\",\\"error\\":{\\"message\\":\\"Some error message\\",\\"name\\":\\"Some error name\\",\\"stack\\":\\"Some error stack\\"},\\"level\\":\\"FATAL\\",\\"message\\":\\"message-1\\",\\"pid\\":5355}"`; exports[`\`format()\` correctly formats record. 2`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-2\\",\\"level\\":\\"ERROR\\",\\"message\\":\\"message-2\\",\\"pid\\":5355}"`; diff --git a/src/core/server/logging/layouts/conversions/timestamp.ts b/src/core/server/logging/layouts/conversions/date.ts similarity index 83% rename from src/core/server/logging/layouts/conversions/timestamp.ts rename to src/core/server/logging/layouts/conversions/date.ts index 6db6fc6eeb6bf..d3ed54fb98240 100644 --- a/src/core/server/logging/layouts/conversions/timestamp.ts +++ b/src/core/server/logging/layouts/conversions/date.ts @@ -22,7 +22,7 @@ import { last } from 'lodash'; import { Conversion } from './type'; import { LogRecord } from '../../log_record'; -const timestampRegExp = /{timestamp({(?[^}]+)})?({(?[^}]+)})?}/gi; +const dateRegExp = /%date({(?[^}]+)})?({(?[^}]+)})?/g; const formats = { ISO8601: 'ISO8601', @@ -54,10 +54,11 @@ function formatDate(date: Date, dateFormat: string = formats.ISO8601, timezone?: } function validateDateFormat(input: string) { - if (Reflect.has(formats, input)) return; - throw new Error( - `Date format expected one of ${Reflect.ownKeys(formats).join(', ')}, but given: ${input}` - ); + if (!Reflect.has(formats, input)) { + throw new Error( + `Date format expected one of ${Reflect.ownKeys(formats).join(', ')}, but given: ${input}` + ); + } } function validateTimezone(timezone: string) { @@ -66,7 +67,7 @@ function validateTimezone(timezone: string) { } function validate(rawString: string) { - for (const matched of rawString.matchAll(timestampRegExp)) { + for (const matched of rawString.matchAll(dateRegExp)) { const { format, timezone } = matched.groups!; if (format) { @@ -78,9 +79,9 @@ function validate(rawString: string) { } } -export const TimestampConversion: Conversion = { - pattern: timestampRegExp, - formatter(record: LogRecord, highlight: boolean, ...matched: any[]) { +export const DateConversion: Conversion = { + pattern: dateRegExp, + convert(record: LogRecord, highlight: boolean, ...matched: any[]) { const groups: Record = last(matched); const { format, timezone } = groups; diff --git a/src/legacy/core_plugins/kibana/server/lib/system_api.js b/src/core/server/logging/layouts/conversions/index.ts similarity index 66% rename from src/legacy/core_plugins/kibana/server/lib/system_api.js rename to src/core/server/logging/layouts/conversions/index.ts index 3e2ab667dd98b..23e6aded6c6f7 100644 --- a/src/legacy/core_plugins/kibana/server/lib/system_api.js +++ b/src/core/server/logging/layouts/conversions/index.ts @@ -16,16 +16,11 @@ * specific language governing permissions and limitations * under the License. */ +export { Conversion } from './type'; -const SYSTEM_API_HEADER_NAME = 'kbn-system-api'; - -/** - * Checks on the *server-side*, if an HTTP request is a system API request - * - * @param request HAPI request object - * @return true if request is a system API request; false, otherwise - * @deprecated Use KibanaRequest#isSystemApi - */ -export function isSystemApiRequest(request) { - return !!request.headers[SYSTEM_API_HEADER_NAME]; -} +export { LoggerConversion } from './logger'; +export { LevelConversion } from './level'; +export { MessageConversion } from './message'; +export { MetaConversion } from './meta'; +export { PidConversion } from './pid'; +export { DateConversion } from './date'; diff --git a/src/core/server/logging/layouts/conversions/level.ts b/src/core/server/logging/layouts/conversions/level.ts index 02ed86dd2c24f..58b271140eff5 100644 --- a/src/core/server/logging/layouts/conversions/level.ts +++ b/src/core/server/logging/layouts/conversions/level.ts @@ -32,8 +32,8 @@ const LEVEL_COLORS = new Map([ ]); export const LevelConversion: Conversion = { - pattern: /{level}/gi, - formatter(record: LogRecord, highlight: boolean) { + pattern: /%level/g, + convert(record: LogRecord, highlight: boolean) { let message = record.level.id.toUpperCase().padEnd(5); if (highlight && LEVEL_COLORS.has(record.level)) { const color = LEVEL_COLORS.get(record.level)!; diff --git a/src/core/server/logging/layouts/conversions/context.ts b/src/core/server/logging/layouts/conversions/logger.ts similarity index 89% rename from src/core/server/logging/layouts/conversions/context.ts rename to src/core/server/logging/layouts/conversions/logger.ts index d1fa9ca84f555..debb1737ab95a 100644 --- a/src/core/server/logging/layouts/conversions/context.ts +++ b/src/core/server/logging/layouts/conversions/logger.ts @@ -22,9 +22,9 @@ import chalk from 'chalk'; import { Conversion } from './type'; import { LogRecord } from '../../log_record'; -export const ContextConversion: Conversion = { - pattern: /{context}/gi, - formatter(record: LogRecord, highlight: boolean) { +export const LoggerConversion: Conversion = { + pattern: /%logger/g, + convert(record: LogRecord, highlight: boolean) { let message = record.context; if (highlight) { message = chalk.magenta(message); diff --git a/src/core/server/logging/layouts/conversions/message.ts b/src/core/server/logging/layouts/conversions/message.ts index b95a89b12b780..f8c5e68ada4fb 100644 --- a/src/core/server/logging/layouts/conversions/message.ts +++ b/src/core/server/logging/layouts/conversions/message.ts @@ -21,8 +21,8 @@ import { Conversion } from './type'; import { LogRecord } from '../../log_record'; export const MessageConversion: Conversion = { - pattern: /{message}/gi, - formatter(record: LogRecord) { + pattern: /%message/g, + convert(record: LogRecord) { // Error stack is much more useful than just the message. return (record.error && record.error.stack) || record.message; }, diff --git a/src/core/server/logging/layouts/conversions/meta.ts b/src/core/server/logging/layouts/conversions/meta.ts index f6d4557e0db53..ee8c207389fbe 100644 --- a/src/core/server/logging/layouts/conversions/meta.ts +++ b/src/core/server/logging/layouts/conversions/meta.ts @@ -20,8 +20,8 @@ import { Conversion } from './type'; import { LogRecord } from '../../log_record'; export const MetaConversion: Conversion = { - pattern: /{meta}/gi, - formatter(record: LogRecord) { - return record.meta ? `[${JSON.stringify(record.meta)}]` : ''; + pattern: /%meta/g, + convert(record: LogRecord) { + return record.meta ? `${JSON.stringify(record.meta)}` : ''; }, }; diff --git a/src/core/server/logging/layouts/conversions/pid.ts b/src/core/server/logging/layouts/conversions/pid.ts index 0fcdd93fcda0c..37d34a4f1cf8b 100644 --- a/src/core/server/logging/layouts/conversions/pid.ts +++ b/src/core/server/logging/layouts/conversions/pid.ts @@ -21,8 +21,8 @@ import { Conversion } from './type'; import { LogRecord } from '../../log_record'; export const PidConversion: Conversion = { - pattern: /{pid}/gi, - formatter(record: LogRecord) { + pattern: /%pid/g, + convert(record: LogRecord) { return String(record.pid); }, }; diff --git a/src/core/server/logging/layouts/conversions/type.ts b/src/core/server/logging/layouts/conversions/type.ts index 34a6475138814..a57a1f954e53a 100644 --- a/src/core/server/logging/layouts/conversions/type.ts +++ b/src/core/server/logging/layouts/conversions/type.ts @@ -20,6 +20,6 @@ import { LogRecord } from 'kibana/server'; export interface Conversion { pattern: RegExp; - formatter: (record: LogRecord, highlight: boolean) => string; + convert: (record: LogRecord, highlight: boolean) => string; validate?: (input: string) => void; } diff --git a/src/core/server/logging/layouts/json_layout.test.ts b/src/core/server/logging/layouts/json_layout.test.ts index ec8c44ec62a22..77e2876c143da 100644 --- a/src/core/server/logging/layouts/json_layout.test.ts +++ b/src/core/server/logging/layouts/json_layout.test.ts @@ -90,34 +90,68 @@ test('`format()` correctly formats record with meta-data', () => { const layout = new JsonLayout(); expect( - layout.format({ - context: 'context-with-meta', - level: LogLevel.Debug, - message: 'message-with-meta', - timestamp, - pid: 5355, - meta: { - from: 'v7', - to: 'v8', - }, - }) - ).toMatchSnapshot(); + JSON.parse( + layout.format({ + context: 'context-with-meta', + level: LogLevel.Debug, + message: 'message-with-meta', + timestamp, + pid: 5355, + meta: { + from: 'v7', + to: 'v8', + }, + }) + ) + ).toStrictEqual({ + '@timestamp': '2012-02-01T09:30:22.011-05:00', + context: 'context-with-meta', + level: 'DEBUG', + message: 'message-with-meta', + meta: { + from: 'v7', + to: 'v8', + }, + pid: 5355, + }); }); test('`format()` correctly formats error record with meta-data', () => { const layout = new JsonLayout(); expect( - layout.format({ - context: 'context-with-meta', - level: LogLevel.Debug, - message: 'message-with-meta', - timestamp, - pid: 5355, - meta: { - from: 'v7', - to: 'v8', - }, - }) - ).toMatchSnapshot(); + JSON.parse( + layout.format({ + context: 'error-with-meta', + level: LogLevel.Debug, + error: { + message: 'Some error message', + name: 'Some error name', + stack: 'Some error stack', + }, + message: 'Some error message', + timestamp, + pid: 5355, + meta: { + from: 'v7', + to: 'v8', + }, + }) + ) + ).toStrictEqual({ + '@timestamp': '2012-02-01T09:30:22.011-05:00', + context: 'error-with-meta', + level: 'DEBUG', + error: { + message: 'Some error message', + name: 'Some error name', + stack: 'Some error stack', + }, + message: 'Some error message', + meta: { + from: 'v7', + to: 'v8', + }, + pid: 5355, + }); }); diff --git a/src/core/server/logging/layouts/layouts.test.ts b/src/core/server/logging/layouts/layouts.test.ts index aa1c54c846bc6..b1fb836f40d5d 100644 --- a/src/core/server/logging/layouts/layouts.test.ts +++ b/src/core/server/logging/layouts/layouts.test.ts @@ -33,12 +33,12 @@ test('`configSchema` creates correct schema for `pattern` layout.', () => { const validConfig = { highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }; expect(layoutsSchema.validate(validConfig)).toEqual({ highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }); const wrongConfig2 = { kind: 'pattern', pattern: 1 }; @@ -56,7 +56,7 @@ test('`create()` creates correct layout.', () => { const patternLayout = Layouts.create({ highlight: false, kind: 'pattern', - pattern: '[{timestamp}][{level}][{context}] {message}', + pattern: '[%date][%level][%logger] %message', }); expect(patternLayout).toBeInstanceOf(PatternLayout); diff --git a/src/core/server/logging/layouts/pattern_layout.test.ts b/src/core/server/logging/layouts/pattern_layout.test.ts index 2d948ea59c6d1..cce55b147e0ed 100644 --- a/src/core/server/logging/layouts/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -88,12 +88,12 @@ test('`createConfigSchema()` creates correct schema.', () => { const validConfig = { highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }; expect(layoutSchema.validate(validConfig)).toEqual({ highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }); const wrongConfig1 = { kind: 'json' }; @@ -112,7 +112,7 @@ test('`format()` correctly formats record with full pattern.', () => { }); test('`format()` correctly formats record with custom pattern.', () => { - const layout = new PatternLayout('mock-{message}-{context}-{message}'); + const layout = new PatternLayout('mock-%message-%logger-%message'); for (const record of records) { expect(layout.format(record)).toMatchSnapshot(); @@ -134,7 +134,7 @@ test('`format()` correctly formats record with meta data.', () => { to: 'v8', }, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta][{"from":"v7","to":"v8"}] message-meta'); + ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{"from":"v7","to":"v8"} message-meta'); expect( layout.format({ @@ -145,7 +145,7 @@ test('`format()` correctly formats record with meta data.', () => { pid: 5355, meta: {}, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta][{}] message-meta'); + ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{} message-meta'); expect( layout.format({ @@ -167,7 +167,7 @@ test('`format()` correctly formats record with highlighting.', () => { }); test('allows specifying the PID in custom pattern', () => { - const layout = new PatternLayout('{pid}-{context}-{message}'); + const layout = new PatternLayout('%pid-%logger-%message'); for (const record of records) { expect(layout.format(record)).toMatchSnapshot(); @@ -175,7 +175,7 @@ test('allows specifying the PID in custom pattern', () => { }); test('`format()` allows specifying pattern with meta.', () => { - const layout = new PatternLayout('{context}-{meta}-{message}'); + const layout = new PatternLayout('%logger-%meta-%message'); const record = { context: 'context', level: LogLevel.Debug, @@ -187,7 +187,7 @@ test('`format()` allows specifying pattern with meta.', () => { to: 'v8', }, }; - expect(layout.format(record)).toBe('context-[{"from":"v7","to":"v8"}]-message'); + expect(layout.format(record)).toBe('context-{"from":"v7","to":"v8"}-message'); }); describe('format', () => { @@ -207,31 +207,31 @@ describe('format', () => { describe('supports specifying a predefined format', () => { it('ISO8601', () => { - const layout = new PatternLayout('[{timestamp{ISO8601}}][{context}]'); + const layout = new PatternLayout('[%date{ISO8601}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][context]'); }); it('ISO8601_TZ', () => { - const layout = new PatternLayout('[{timestamp{ISO8601_TZ}}][{context}]'); + const layout = new PatternLayout('[%date{ISO8601_TZ}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T09:30:22.011-05:00][context]'); }); it('ABSOLUTE', () => { - const layout = new PatternLayout('[{timestamp{ABSOLUTE}}][{context}]'); + const layout = new PatternLayout('[%date{ABSOLUTE}][%logger]'); expect(layout.format(record)).toBe('[09:30:22.011][context]'); }); it('UNIX', () => { - const layout = new PatternLayout('[{timestamp{UNIX}}][{context}]'); + const layout = new PatternLayout('[%date{UNIX}][%logger]'); expect(layout.format(record)).toBe('[1328106622][context]'); }); it('UNIX_MILLIS', () => { - const layout = new PatternLayout('[{timestamp{UNIX_MILLIS}}][{context}]'); + const layout = new PatternLayout('[%date{UNIX_MILLIS}][%logger]'); expect(layout.format(record)).toBe('[1328106622011][context]'); }); @@ -239,42 +239,38 @@ describe('format', () => { describe('supports specifying a predefined format and timezone', () => { it('ISO8601', () => { - const layout = new PatternLayout('[{timestamp{ISO8601}{America/Los_Angeles}}][{context}]'); + const layout = new PatternLayout('[%date{ISO8601}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][context]'); }); it('ISO8601_TZ', () => { - const layout = new PatternLayout( - '[{timestamp{ISO8601_TZ}{America/Los_Angeles}}][{context}]' - ); + const layout = new PatternLayout('[%date{ISO8601_TZ}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T06:30:22.011-08:00][context]'); }); it('ABSOLUTE', () => { - const layout = new PatternLayout('[{timestamp{ABSOLUTE}{America/Los_Angeles}}][{context}]'); + const layout = new PatternLayout('[%date{ABSOLUTE}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[06:30:22.011][context]'); }); it('UNIX', () => { - const layout = new PatternLayout('[{timestamp{UNIX}{America/Los_Angeles}}][{context}]'); + const layout = new PatternLayout('[%date{UNIX}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[1328106622][context]'); }); it('UNIX_MILLIS', () => { - const layout = new PatternLayout( - '[{timestamp{UNIX_MILLIS}{America/Los_Angeles}}][{context}]' - ); + const layout = new PatternLayout('[%date{UNIX_MILLIS}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[1328106622011][context]'); }); }); it('formats several conversions patterns correctly', () => { const layout = new PatternLayout( - '[{timestamp{ABSOLUTE}{America/Los_Angeles}}][{context}][{timestamp{UNIX}}]' + '[%date{ABSOLUTE}{America/Los_Angeles}][%logger][%date{UNIX}]' ); expect(layout.format(record)).toBe('[06:30:22.011][context][1328106622]'); @@ -284,45 +280,44 @@ describe('format', () => { describe('schema', () => { describe('pattern', () => { - describe('{timestamp}', () => { - it('does not fail when {timestamp} not present', () => { + describe('%date', () => { + it('does not fail when %date not present', () => { expect(patternSchema.validate('')).toBe(''); expect(patternSchema.validate('{pid}')).toBe('{pid}'); }); - it('does not fail on {timestamp} without params', () => { - expect(patternSchema.validate('{timestamp}')).toBe('{timestamp}'); - expect(patternSchema.validate('{timestamp}}')).toBe('{timestamp}}'); - expect(patternSchema.validate('{{timestamp}}')).toBe('{{timestamp}}'); + it('does not fail on %date without params', () => { + expect(patternSchema.validate('%date')).toBe('%date'); + expect(patternSchema.validate('%date')).toBe('%date'); + expect(patternSchema.validate('{%date}')).toBe('{%date}'); + expect(patternSchema.validate('%date%date')).toBe('%date%date'); }); - it('does not fail on {timestamp} with predefined date format', () => { - expect(patternSchema.validate('{timestamp{ISO8601}}')).toBe('{timestamp{ISO8601}}'); + it('does not fail on %date with predefined date format', () => { + expect(patternSchema.validate('%date{ISO8601}')).toBe('%date{ISO8601}'); }); - it('does not fail on {timestamp} with predefined date format and valid timezone', () => { - expect(patternSchema.validate('{timestamp{ISO8601_TZ}{Europe/Berlin}}')).toBe( - '{timestamp{ISO8601_TZ}{Europe/Berlin}}' + it('does not fail on %date with predefined date format and valid timezone', () => { + expect(patternSchema.validate('%date{ISO8601_TZ}{Europe/Berlin}')).toBe( + '%date{ISO8601_TZ}{Europe/Berlin}' ); }); - it('fails on {timestamp} with unknown date format', () => { - expect(() => - patternSchema.validate('{timestamp{HH:MM:SS}}') - ).toThrowErrorMatchingInlineSnapshot( + it('fails on %date with unknown date format', () => { + expect(() => patternSchema.validate('%date{HH:MM:SS}')).toThrowErrorMatchingInlineSnapshot( `"Date format expected one of ISO8601, ISO8601_TZ, ABSOLUTE, UNIX, UNIX_MILLIS, but given: HH:MM:SS"` ); }); - it('fails on {timestamp} with predefined date format and invalid timezone', () => { + it('fails on %date with predefined date format and invalid timezone', () => { expect(() => - patternSchema.validate('{timestamp{ISO8601_TZ}{Europe/Kibana}}') + patternSchema.validate('%date{ISO8601_TZ}{Europe/Kibana}') ).toThrowErrorMatchingInlineSnapshot(`"Unknown timezone: Europe/Kibana"`); }); - it('validates several {timestamp} in pattern', () => { + it('validates several %date in pattern', () => { expect(() => - patternSchema.validate('{timestamp{ISO8601_TZ}{Europe/Berlin}}{message}{timestamp{HH}}') + patternSchema.validate('%date{ISO8601_TZ}{Europe/Berlin}%message%date{HH}') ).toThrowErrorMatchingInlineSnapshot( `"Date format expected one of ISO8601, ISO8601_TZ, ABSOLUTE, UNIX, UNIX_MILLIS, but given: HH"` ); diff --git a/src/core/server/logging/layouts/pattern_layout.ts b/src/core/server/logging/layouts/pattern_layout.ts index 0a2a25a135069..9490db149cc0f 100644 --- a/src/core/server/logging/layouts/pattern_layout.ts +++ b/src/core/server/logging/layouts/pattern_layout.ts @@ -21,23 +21,24 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { LogRecord } from '../log_record'; import { Layout } from './layouts'; - -import { Conversion } from './conversions/type'; -import { ContextConversion } from './conversions/context'; -import { LevelConversion } from './conversions/level'; -import { MetaConversion } from './conversions/meta'; -import { MessageConversion } from './conversions/message'; -import { PidConversion } from './conversions/pid'; -import { TimestampConversion } from './conversions/timestamp'; +import { + Conversion, + LoggerConversion, + LevelConversion, + MetaConversion, + MessageConversion, + PidConversion, + DateConversion, +} from './conversions'; /** * Default pattern used by PatternLayout if it's not overridden in the configuration. */ -const DEFAULT_PATTERN = `[{timestamp}][{level}][{context}]{meta} {message}`; +const DEFAULT_PATTERN = `[%date][%level][%logger]%meta %message`; export const patternSchema = schema.string({ validate: string => { - TimestampConversion.validate!(string); + DateConversion.validate!(string); }, }); @@ -48,12 +49,12 @@ const patternLayoutSchema = schema.object({ }); const conversions: Conversion[] = [ - ContextConversion, + LoggerConversion, MessageConversion, LevelConversion, MetaConversion, PidConversion, - TimestampConversion, + DateConversion, ]; /** @internal */ @@ -77,7 +78,7 @@ export class PatternLayout implements Layout { for (const conversion of conversions) { recordString = recordString.replace( conversion.pattern, - conversion.formatter.bind(null, record, this.highlight) + conversion.convert.bind(null, record, this.highlight) ); } diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index b3631abb9ff00..75f571d34c25c 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -59,7 +59,7 @@ test('`getLoggerContext()` returns correct joined context name.', () => { test('correctly fills in default config.', () => { const configValue = new LoggingConfig(config.schema.validate({})); - expect(configValue.appenders.size).toBe(3); + expect(configValue.appenders.size).toBe(2); expect(configValue.appenders.get('default')).toEqual({ kind: 'console', @@ -69,10 +69,6 @@ test('correctly fills in default config.', () => { kind: 'console', layout: { kind: 'pattern', highlight: true }, }); - expect(configValue.appenders.get('file')).toEqual({ - kind: 'file', - layout: { kind: 'pattern', highlight: false }, - }); }); test('correctly fills in custom `appenders` config.', () => { @@ -83,16 +79,11 @@ test('correctly fills in custom `appenders` config.', () => { kind: 'console', layout: { kind: 'pattern' }, }, - file: { - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', - }, }, }) ); - expect(configValue.appenders.size).toBe(3); + expect(configValue.appenders.size).toBe(2); expect(configValue.appenders.get('default')).toEqual({ kind: 'console', @@ -103,12 +94,6 @@ test('correctly fills in custom `appenders` config.', () => { kind: 'console', layout: { kind: 'pattern' }, }); - - expect(configValue.appenders.get('file')).toEqual({ - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', - }); }); test('correctly fills in default `loggers` config.', () => { diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index f1fbf787737b4..8f80be7d79cb1 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -140,13 +140,6 @@ export class LoggingConfig { layout: { kind: 'pattern', highlight: true }, } as AppenderConfigType, ], - [ - 'file', - { - kind: 'file', - layout: { kind: 'pattern', highlight: false }, - } as AppenderConfigType, - ], ]); /** diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 395e0da218307..36563ba8cbe45 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -25,7 +25,6 @@ import { migrations } from './migrations'; import { importApi } from './server/routes/api/import'; import { exportApi } from './server/routes/api/export'; import { managementApi } from './server/routes/api/management'; -import * as systemApi from './server/lib/system_api'; import mappings from './mappings.json'; import { getUiSettingDefaults } from './ui_setting_defaults'; import { registerCspCollector } from './server/lib/csp_usage_collector'; @@ -323,7 +322,6 @@ export default function(kibana) { exportApi(server); managementApi(server); registerCspCollector(usageCollection, server); - server.expose('systemApi', systemApi); server.injectUiAppVars('kibana', () => injectVars(server)); }, }); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap index e08d802406fff..661d1d33a5283 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap @@ -14,6 +14,7 @@ exports[`should render popover when appLinks is not empty 1`] = ` } closePopover={[Function]} + data-test-subj="launchSampleDataSetecommerce" display="inlineBlock" hasArrow={true} id="sampleDataLinksecommerce" diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js index e6f5c07c94f9f..cb43c18a8e78b 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js @@ -112,6 +112,7 @@ export class SampleDataViewDataButton extends React.Component { closePopover={this.closePopover} panelPaddingSize="none" anchorPosition="downCenter" + data-test-subj={`launchSampleDataSet${this.props.id}`} > diff --git a/src/legacy/core_plugins/kibana/server/lib/__tests__/system_api.js b/src/legacy/core_plugins/kibana/server/lib/__tests__/system_api.js deleted file mode 100644 index a63a93f3a70d5..0000000000000 --- a/src/legacy/core_plugins/kibana/server/lib/__tests__/system_api.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { isSystemApiRequest } from '../system_api'; - -describe('system_api', () => { - describe('#isSystemApiRequest', () => { - it('returns true for a system API HTTP request', () => { - const mockHapiRequest = { - headers: { - 'kbn-system-api': true, - }, - }; - expect(isSystemApiRequest(mockHapiRequest)).to.be(true); - }); - - it('returns false for a non-system API HTTP request', () => { - const mockHapiRequest = { - headers: {}, - }; - expect(isSystemApiRequest(mockHapiRequest)).to.be(false); - }); - }); -}); diff --git a/src/legacy/ui/public/system_api/__tests__/system_api.js b/src/legacy/ui/public/system_api/__tests__/system_api.js index 822edaa08fdd6..816024f13f8b2 100644 --- a/src/legacy/ui/public/system_api/__tests__/system_api.js +++ b/src/legacy/ui/public/system_api/__tests__/system_api.js @@ -31,8 +31,8 @@ describe('system_api', () => { }; const newHeaders = addSystemApiHeader(headers); - expect(newHeaders).to.have.property('kbn-system-api'); - expect(newHeaders['kbn-system-api']).to.be(true); + expect(newHeaders).to.have.property('kbn-system-request'); + expect(newHeaders['kbn-system-request']).to.be(true); expect(newHeaders).to.have.property('kbn-version'); expect(newHeaders['kbn-version']).to.be('4.6.0'); @@ -40,7 +40,16 @@ describe('system_api', () => { }); describe('#isSystemApiRequest', () => { - it('returns true for a system API HTTP request', () => { + it('returns true for a system HTTP request', () => { + const mockRequest = { + headers: { + 'kbn-system-request': true, + }, + }; + expect(isSystemApiRequest(mockRequest)).to.be(true); + }); + + it('returns true for a legacy system API HTTP request', () => { const mockRequest = { headers: { 'kbn-system-api': true, diff --git a/src/plugins/kibana_legacy/public/utils/system_api.ts b/src/plugins/kibana_legacy/public/utils/system_api.ts index 397de4dcc2bb3..49d4a78584737 100644 --- a/src/plugins/kibana_legacy/public/utils/system_api.ts +++ b/src/plugins/kibana_legacy/public/utils/system_api.ts @@ -19,7 +19,8 @@ import { IRequestConfig } from 'angular'; -const SYSTEM_API_HEADER_NAME = 'kbn-system-api'; +const SYSTEM_REQUEST_HEADER_NAME = 'kbn-system-request'; +const LEGACY_SYSTEM_API_HEADER_NAME = 'kbn-system-api'; /** * Adds a custom header designating request as system API @@ -28,7 +29,7 @@ const SYSTEM_API_HEADER_NAME = 'kbn-system-api'; */ export function addSystemApiHeader(originalHeaders: Record) { const systemApiHeaders = { - [SYSTEM_API_HEADER_NAME]: true, + [SYSTEM_REQUEST_HEADER_NAME]: true, }; return { ...originalHeaders, @@ -44,5 +45,7 @@ export function addSystemApiHeader(originalHeaders: Record) { */ export function isSystemApiRequest(request: IRequestConfig) { const { headers } = request; - return headers && !!headers[SYSTEM_API_HEADER_NAME]; + return ( + headers && (!!headers[SYSTEM_REQUEST_HEADER_NAME] || !!headers[LEGACY_SYSTEM_API_HEADER_NAME]) + ); } diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 8088b5a0f9da9..8bc528e045566 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -84,7 +84,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should launch sample flights data set dashboard', async () => { - await PageObjects.home.launchSampleDataSet('flights'); + await PageObjects.home.launchSampleDashboard('flights'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); const todayYearMonthDay = moment().format('MMM D, YYYY'); @@ -96,7 +96,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should render visualizations', async () => { - await PageObjects.home.launchSampleDataSet('flights'); + await PageObjects.home.launchSampleDashboard('flights'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); log.debug('Checking pie charts rendered'); @@ -115,7 +115,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should launch sample logs data set dashboard', async () => { - await PageObjects.home.launchSampleDataSet('logs'); + await PageObjects.home.launchSampleDashboard('logs'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); const todayYearMonthDay = moment().format('MMM D, YYYY'); @@ -127,7 +127,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should launch sample ecommerce data set dashboard', async () => { - await PageObjects.home.launchSampleDataSet('ecommerce'); + await PageObjects.home.launchSampleDashboard('ecommerce'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); const todayYearMonthDay = moment().format('MMM D, YYYY'); diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index a641fbda023c3..6225b4e3aca62 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -19,9 +19,12 @@ import { FtrProviderContext } from '../ftr_provider_context'; -export function HomePageProvider({ getService }: FtrProviderContext) { +export function HomePageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const find = getService('find'); + const PageObjects = getPageObjects(['common']); + let isOss = true; class HomePage { async clickSynopsis(title: string) { @@ -63,6 +66,14 @@ export function HomePageProvider({ getService }: FtrProviderContext) { }); } + async launchSampleDashboard(id: string) { + await this.launchSampleDataSet(id); + isOss = await PageObjects.common.isOss(); + if (!isOss) { + await find.clickByLinkText('Dashboard'); + } + } + async launchSampleDataSet(id: string) { await this.addSampleDataSet(id); await testSubjects.click(`launchSampleDataSet${id}`); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index a45357121354f..ed09b71f0c31c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -305,7 +305,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` exports[`ErrorGroupOverview -> List should render with data 1`] = ` .c0 { - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; + font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; } .c1 { @@ -316,7 +316,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` } .c2 { - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; + font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; font-size: 16px; max-width: 100%; white-space: nowrap; @@ -325,7 +325,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` } .c3 { - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; + font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; } new Promise(resolve => setTimeout(resolve)); +const waitForPromises = async () => + act(async () => { + await new Promise(resolve => setTimeout(resolve)); + }); function createMockFrame(): jest.Mocked { return { @@ -220,6 +223,7 @@ describe('Lens App', () => { }); instance.setProps({ docId: '1234' }); + await waitForPromises(); expect(defaultArgs.core.chrome.setBreadcrumbs).toHaveBeenCalledWith([ @@ -373,8 +377,10 @@ describe('Lens App', () => { async function save({ initialDocId, addToDashboardMode, + lastKnownDoc = { expression: 'kibana 3' }, ...saveProps }: SaveProps & { + lastKnownDoc?: object; initialDocId?: string; addToDashboardMode?: boolean; }) { @@ -392,6 +398,7 @@ describe('Lens App', () => { state: { query: 'fake query', datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + filters: [], }, }); (args.docStorage.save as jest.Mock).mockImplementation(async ({ id }) => ({ @@ -410,10 +417,12 @@ describe('Lens App', () => { } const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: initialDocId, expression: 'kibana 3' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: { id: initialDocId, ...lastKnownDoc } as Document, + }) + ); instance.update(); @@ -441,10 +450,12 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(true); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + }) + ); instance.update(); expect(getButton(instance).disableButton).toEqual(true); }); @@ -482,10 +493,12 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(true); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + }) + ); instance.update(); expect(getButton(instance).disableButton).toEqual(false); @@ -559,10 +572,12 @@ describe('Lens App', () => { const instance = mount(); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: undefined, expression: 'new expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: undefined, expression: 'new expression' } as unknown) as Document, + }) + ); instance.update(); @@ -593,6 +608,38 @@ describe('Lens App', () => { expect(args.redirectTo).toHaveBeenCalledWith('aaa'); }); + + it('saves app filters and does not save pinned filters', async () => { + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const field = ({ name: 'myfield' } as unknown) as IFieldType; + const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + + const unpinned = esFilters.buildExistsFilter(field, indexPattern); + const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); + FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); + await waitForPromises(); + + const { args } = await save({ + initialDocId: '1234', + newCopyOnSave: false, + newTitle: 'hello there2', + lastKnownDoc: { + expression: 'kibana 3', + state: { + filters: [pinned, unpinned], + }, + }, + }); + + expect(args.docStorage.save).toHaveBeenCalledWith({ + id: '1234', + title: 'hello there2', + expression: 'kibana 3', + state: { + filters: [unpinned], + }, + }); + }); }); }); @@ -658,10 +705,12 @@ describe('Lens App', () => { ); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [{ id: '1', title: 'newIndex' }], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [{ id: '1', title: 'newIndex' }], + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + }) + ); await waitForPromises(); instance.update(); @@ -674,12 +723,15 @@ describe('Lens App', () => { ); // Do it again to verify that the dirty checking is done right - onChange({ - filterableIndexPatterns: [{ id: '2', title: 'second index' }], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [{ id: '2', title: 'second index' }], + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + }) + ); await waitForPromises(); + instance.update(); expect(TopNavMenu).toHaveBeenLastCalledWith( @@ -689,17 +741,18 @@ describe('Lens App', () => { {} ); }); - it('updates the editor frame when the user changes query or time in the search bar', () => { const args = defaultArgs; args.editorFrame = frame; const instance = mount(); - instance.find(TopNavMenu).prop('onQuerySubmit')!({ - dateRange: { from: 'now-14d', to: 'now-7d' }, - query: { query: 'new', language: 'lucene' }, - }); + act(() => + instance.find(TopNavMenu).prop('onQuerySubmit')!({ + dateRange: { from: 'now-14d', to: 'now-7d' }, + query: { query: 'new', language: 'lucene' }, + }) + ); instance.update(); @@ -728,7 +781,9 @@ describe('Lens App', () => { const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; - args.data.query.filterManager.setFilters([esFilters.buildExistsFilter(field, indexPattern)]); + act(() => + args.data.query.filterManager.setFilters([esFilters.buildExistsFilter(field, indexPattern)]) + ); instance.update(); @@ -852,10 +907,12 @@ describe('Lens App', () => { const instance = mount(); - instance.find(TopNavMenu).prop('onQuerySubmit')!({ - dateRange: { from: 'now-14d', to: 'now-7d' }, - query: { query: 'new', language: 'lucene' }, - }); + act(() => + instance.find(TopNavMenu).prop('onQuerySubmit')!({ + dateRange: { from: 'now-14d', to: 'now-7d' }, + query: { query: 'new', language: 'lucene' }, + }) + ); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; @@ -865,10 +922,10 @@ describe('Lens App', () => { const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); - args.data.query.filterManager.setFilters([pinned, unpinned]); + act(() => args.data.query.filterManager.setFilters([pinned, unpinned])); instance.update(); - instance.find(TopNavMenu).prop('onClearSavedQuery')!(); + act(() => instance.find(TopNavMenu).prop('onClearSavedQuery')!()); instance.update(); expect(frame.mount).toHaveBeenLastCalledWith( diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index c901d4c0c1497..a212cb0a1a879 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -20,6 +20,7 @@ import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; import { + esFilters, Filter, IndexPattern as IndexPatternInstance, IndexPatternsContract, @@ -320,8 +321,22 @@ export function App({ {lastKnownDoc && state.isSaveModalVisible && ( { + const [pinnedFilters, appFilters] = _.partition( + lastKnownDoc.state?.filters, + esFilters.isFilterPinned + ); + const lastDocWithoutPinned = pinnedFilters?.length + ? { + ...lastKnownDoc, + state: { + ...lastKnownDoc.state, + filters: appFilters, + }, + } + : lastKnownDoc; + const doc = { - ...lastKnownDoc, + ...lastDocWithoutPinned, id: props.newCopyOnSave ? undefined : lastKnownDoc.id, title: props.newTitle, }; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/index.ts b/x-pack/legacy/plugins/lens/public/app_plugin/index.ts index f75dce9b7507f..1460fdfef37e6 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/index.ts +++ b/x-pack/legacy/plugins/lens/public/app_plugin/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export * from './app'; diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_visualization.scss b/x-pack/legacy/plugins/lens/public/datatable_visualization/_visualization.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_visualization.scss rename to x-pack/legacy/plugins/lens/public/datatable_visualization/_visualization.scss diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx rename to x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts b/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts new file mode 100644 index 0000000000000..6dee47cc632c2 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/public'; +import { datatableVisualization } from './visualization'; +import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { datatable, datatableColumns, getDatatableRenderer } from './expression'; +import { FormatFactory } from '../legacy_imports'; +import { EditorFrameSetup } from '../types'; + +export interface DatatableVisualizationPluginSetupPlugins { + expressions: ExpressionsSetup; + formatFactory: FormatFactory; + editorFrame: EditorFrameSetup; +} + +export class DatatableVisualization { + constructor() {} + + setup( + _core: CoreSetup | null, + { expressions, formatFactory, editorFrame }: DatatableVisualizationPluginSetupPlugins + ) { + expressions.registerFunction(() => datatableColumns); + expressions.registerFunction(() => datatable); + expressions.registerRenderer(() => getDatatableRenderer(formatFactory)); + editorFrame.registerVisualization(datatableVisualization); + } +} diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx rename to x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx index cb9350226575c..0cba22170df1f 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockDatasource } from '../editor_frame_service/mocks'; import { DatatableVisualizationState, datatableVisualization, diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx rename to x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx deleted file mode 100644 index ed047f52ecc0f..0000000000000 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npSetup } from 'ui/new_platform'; -import { CoreSetup } from 'src/core/public'; -import { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; -import { datatableVisualization } from './visualization'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; -import { datatable, datatableColumns, getDatatableRenderer } from './expression'; - -export interface DatatableVisualizationPluginSetupPlugins { - expressions: ExpressionsSetup; - // TODO this is a simulated NP plugin. - // Once field formatters are actually migrated, the actual shim can be used - fieldFormat: { - formatFactory: FormatFactory; - }; -} - -class DatatableVisualizationPlugin { - constructor() {} - - setup( - _core: CoreSetup | null, - { expressions, fieldFormat }: DatatableVisualizationPluginSetupPlugins - ) { - expressions.registerFunction(() => datatableColumns); - expressions.registerFunction(() => datatable); - expressions.registerRenderer(() => getDatatableRenderer(fieldFormat.formatFactory)); - - return datatableVisualization; - } - - stop() {} -} - -const plugin = new DatatableVisualizationPlugin(); - -export const datatableVisualizationSetup = () => - plugin.setup(npSetup.core, { - expressions: npSetup.plugins.expressions, - fieldFormat: { - formatFactory: getFormat, - }, - }); -export const datatableVisualizationStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/__mocks__/suggestion_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/__mocks__/suggestion_helpers.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_chart_switch.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_chart_switch.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_data_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_data_panel_wrapper.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_expression_renderer.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_expression_renderer.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_frame_layout.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_frame_layout.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts similarity index 78% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 00cde2ee3e04c..e8bb8914fa292 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; -import { Chrome } from 'ui/chrome'; - -import { capabilities } from 'ui/capabilities'; +import { + Capabilities, + HttpSetup, + RecursiveReadonly, + SavedObjectsClientContract, +} from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { IndexPatternsContract, IndexPattern } from '../../../../../../../src/plugins/data/public'; import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; @@ -24,14 +26,12 @@ import { getEditPath } from '../../../../../../plugins/lens/common'; export class EmbeddableFactory extends AbstractEmbeddableFactory { type = DOC_TYPE; - private chrome: Chrome; - private indexPatternService: IndexPatternsContract; - private expressionRenderer: ReactExpressionRendererType; - constructor( - chrome: Chrome, - expressionRenderer: ReactExpressionRendererType, - indexPatternService: IndexPatternsContract + private coreHttp: HttpSetup, + private capabilities: RecursiveReadonly, + private savedObjectsClient: SavedObjectsClientContract, + private expressionRenderer: ReactExpressionRendererType, + private indexPatternService: IndexPatternsContract ) { super({ savedObjectMetaData: { @@ -42,13 +42,10 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { getIconForSavedObject: () => 'lensApp', }, }); - this.chrome = chrome; - this.expressionRenderer = expressionRenderer; - this.indexPatternService = indexPatternService; } public isEditable() { - return capabilities.get().visualize.save as boolean; + return this.capabilities.visualize.save as boolean; } canCreateNew() { @@ -66,7 +63,7 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { input: Partial & { id: string }, parent?: IContainer ) { - const store = new SavedObjectIndexStore(this.chrome.getSavedObjectsClient()); + const store = new SavedObjectIndexStore(this.savedObjectsClient); const savedVis = await store.load(savedObjectId); const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map( @@ -91,7 +88,7 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { this.expressionRenderer, { savedVis, - editUrl: this.chrome.addBasePath(getEditPath(savedObjectId)), + editUrl: this.coreHttp.basePath.prepend(getEditPath(savedObjectId)), editable: this.isEditable(), indexPatterns, }, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts similarity index 90% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts index f75dce9b7507f..d6e96d74b766c 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export * from './service'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts index 3c466522e1ebe..c5be5f524755d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts @@ -11,7 +11,7 @@ import { KibanaDatatable, } from 'src/plugins/expressions/public'; import { LensMultiTable } from '../types'; -import { toAbsoluteDates } from '../indexpattern_plugin/auto_date'; +import { toAbsoluteDates } from '../indexpattern_datasource/auto_date'; interface MergeTables { layerIds: string[]; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx similarity index 95% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx index b4fc88cb074c7..cd121a1f96a2b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx @@ -10,12 +10,10 @@ import { ExpressionsSetup, ExpressionsStart, } from '../../../../../../src/plugins/expressions/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { embeddablePluginMock } from '../../../../../../src/plugins/embeddable/public/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; import { DatasourcePublicAPI, FramePublicAPI, Datasource, Visualization } from '../types'; -import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './plugin'; +import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './service'; export function createMockVisualization(): jest.Mocked { return { @@ -108,9 +106,6 @@ export function createMockSetupDependencies() { data: {}, embeddable: embeddablePluginMock.createSetupContract(), expressions: expressionsPluginMock.createSetupContract(), - chrome: { - getSavedObjectsClient: () => {}, - }, } as unknown) as MockedSetupDependencies; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx similarity index 66% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx index 7a6067dd5f23c..ef4b5f6d7b834 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EditorFramePlugin } from './plugin'; +import { EditorFrameService } from './service'; import { coreMock } from 'src/core/public/mocks'; import { MockedSetupDependencies, @@ -25,14 +25,14 @@ jest.mock('./embeddable/embeddable_factory', () => ({ EmbeddableFactory: class Mock {}, })); -describe('editor_frame plugin', () => { - let pluginInstance: EditorFramePlugin; +describe('editor_frame service', () => { + let pluginInstance: EditorFrameService; let mountpoint: Element; let pluginSetupDependencies: MockedSetupDependencies; let pluginStartDependencies: MockedStartDependencies; beforeEach(() => { - pluginInstance = new EditorFramePlugin(); + pluginInstance = new EditorFrameService(); mountpoint = document.createElement('div'); pluginSetupDependencies = createMockSetupDependencies(); pluginStartDependencies = createMockStartDependencies(); @@ -42,26 +42,28 @@ describe('editor_frame plugin', () => { mountpoint.remove(); }); - it('should create an editor frame instance which mounts and unmounts', () => { - expect(() => { - pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); - const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = publicAPI.createInstance({}); - instance.mount(mountpoint, { - onError: jest.fn(), - onChange: jest.fn(), - dateRange: { fromDate: '', toDate: '' }, - query: { query: '', language: 'lucene' }, - filters: [], - }); - instance.unmount(); - }).not.toThrowError(); + it('should create an editor frame instance which mounts and unmounts', async () => { + await expect( + (async () => { + pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); + const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); + const instance = await publicAPI.createInstance({}); + instance.mount(mountpoint, { + onError: jest.fn(), + onChange: jest.fn(), + dateRange: { fromDate: '', toDate: '' }, + query: { query: '', language: 'lucene' }, + filters: [], + }); + instance.unmount(); + })() + ).resolves.toBeUndefined(); }); - it('should not have child nodes after unmount', () => { + it('should not have child nodes after unmount', async () => { pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = publicAPI.createInstance({}); + const instance = await publicAPI.createInstance({}); instance.mount(mountpoint, { onError: jest.fn(), onChange: jest.fn(), diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx similarity index 70% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx index e914eb7d7784b..9a3d724705a1a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx @@ -8,8 +8,6 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { CoreSetup, CoreStart } from 'src/core/public'; -import chrome, { Chrome } from 'ui/chrome'; -import { npSetup, npStart } from 'ui/new_platform'; import { ExpressionsSetup, ExpressionsStart, @@ -44,24 +42,35 @@ export interface EditorFrameStartPlugins { data: DataPublicPluginStart; embeddable: IEmbeddableStart; expressions: ExpressionsStart; - chrome: Chrome; } -export class EditorFramePlugin { +async function collectAsyncDefinitions( + definitions: Array> +) { + const resolvedDefinitions = await Promise.all(definitions); + const definitionMap: Record = {}; + resolvedDefinitions.forEach(definition => { + definitionMap[definition.id] = definition; + }); + + return definitionMap; +} + +export class EditorFrameService { constructor() {} - private readonly datasources: Record = {}; - private readonly visualizations: Record = {}; + private readonly datasources: Array> = []; + private readonly visualizations: Array> = []; public setup(core: CoreSetup, plugins: EditorFrameSetupPlugins): EditorFrameSetup { plugins.expressions.registerFunction(() => mergeTables); return { registerDatasource: datasource => { - this.datasources[datasource.id] = datasource as Datasource; + this.datasources.push(datasource as Datasource); }, registerVisualization: visualization => { - this.visualizations[visualization.id] = visualization as Visualization; + this.visualizations.push(visualization as Visualization); }, }; } @@ -70,27 +79,34 @@ export class EditorFramePlugin { plugins.embeddable.registerEmbeddableFactory( 'lens', new EmbeddableFactory( - plugins.chrome, + core.http, + core.application.capabilities, + core.savedObjects.client, plugins.expressions.ReactExpressionRenderer, plugins.data.indexPatterns ) ); - const createInstance = (): EditorFrameInstance => { + const createInstance = async (): Promise => { let domElement: Element; + const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ + collectAsyncDefinitions(this.datasources), + collectAsyncDefinitions(this.visualizations), + ]); + return { mount: (element, { doc, onError, dateRange, query, filters, savedQuery, onChange }) => { domElement = element; - const firstDatasourceId = Object.keys(this.datasources)[0]; - const firstVisualizationId = Object.keys(this.visualizations)[0]; + const firstDatasourceId = Object.keys(resolvedDatasources)[0]; + const firstVisualizationId = Object.keys(resolvedVisualizations)[0]; render( - editorFrame.setup(npSetup.core, { - data: npSetup.plugins.data, - embeddable: npSetup.plugins.embeddable, - expressions: npSetup.plugins.expressions, - }); - -export const editorFrameStart = () => - editorFrame.start(npStart.core, { - data: npStart.plugins.data, - embeddable: npStart.plugins.embeddable, - expressions: npStart.plugins.expressions, - chrome, - }); - -export const editorFrameStop = () => editorFrame.stop(); diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index f646b1ed0a9ae..496573f6a1c9a 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -7,9 +7,9 @@ @import './config_panel'; @import './app_plugin/index'; -@import './datatable_visualization_plugin/index'; +@import 'datatable_visualization/index'; @import './drag_drop/index'; -@import './editor_frame_plugin/index'; -@import './indexpattern_plugin/index'; -@import './xy_visualization_plugin/index'; -@import './metric_visualization_plugin/index'; +@import 'editor_frame_service/index'; +@import 'indexpattern_datasource/index'; +@import 'xy_visualization/index'; +@import 'metric_visualization/index'; diff --git a/x-pack/legacy/plugins/lens/public/index.ts b/x-pack/legacy/plugins/lens/public/index.ts index 9f4141dbcae7d..e49f648906af0 100644 --- a/x-pack/legacy/plugins/lens/public/index.ts +++ b/x-pack/legacy/plugins/lens/public/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LensPlugin } from './plugin'; + export * from './types'; + +export const plugin = () => new LensPlugin(); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/loader.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__snapshots__/lens_field_icon.test.tsx.snap b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/__snapshots__/lens_field_icon.test.tsx.snap rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_field_item.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/_field_item.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_dimension_panel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_dimension_panel.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_field_select.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_field_select.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_field_select.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_field_select.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_index.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_popover.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_popover.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/document_field.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/document_field.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/document_field.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/document_field.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts new file mode 100644 index 0000000000000..3ca6e3e1ef56e --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/public'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { getIndexPatternDatasource } from './indexpattern'; +import { renameColumns } from './rename_columns'; +import { autoDate } from './auto_date'; +import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, +} from '../../../../../../src/plugins/data/public'; +import { Datasource, EditorFrameSetup } from '../types'; + +export interface IndexPatternDatasourceSetupPlugins { + expressions: ExpressionsSetup; + data: DataPublicPluginSetup; + editorFrame: EditorFrameSetup; +} + +export interface IndexPatternDatasourceStartPlugins { + data: DataPublicPluginStart; +} + +export class IndexPatternDatasource { + constructor() {} + + setup( + core: CoreSetup, + { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins + ) { + expressions.registerFunction(renameColumns); + expressions.registerFunction(autoDate); + + editorFrame.registerDatasource( + core.getStartServices().then(([coreStart, { data }]) => + getIndexPatternDatasource({ + core: coreStart, + storage: new Storage(localStorage), + data, + }) + ) as Promise + ); + } +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts similarity index 97% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index e7def3b9dbf2c..41be22f2c72ed 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chromeMock from 'ui/chrome'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { SavedObjectsClientContract } from 'kibana/public'; import { getIndexPatternDatasource, IndexPatternColumn, uniqueLabels } from './indexpattern'; import { DatasourcePublicAPI, Operation, Datasource } from '../types'; import { coreMock } from 'src/core/public/mocks'; @@ -15,8 +13,6 @@ import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; jest.mock('./loader'); jest.mock('../id_generator'); -// chrome, notify, storage are used by ./plugin -jest.mock('ui/chrome'); // Contains old and new platform data plugins, used for interpreter and filter ratio jest.mock('ui/new_platform'); @@ -142,10 +138,8 @@ describe('IndexPattern Data Source', () => { beforeEach(() => { indexPatternDatasource = getIndexPatternDatasource({ - chrome: chromeMock, storage: {} as IStorageWrapper, core: coreMock.createStart(), - savedObjectsClient: {} as SavedObjectsClientContract, data: pluginsMock.createStart().data, }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 2426d7fc14b5d..afb88d1af7951 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart, SavedObjectsClientContract } from 'src/core/public'; +import { CoreStart } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { @@ -21,7 +21,6 @@ import { import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader'; import { toExpression } from './to_expression'; import { IndexPatternDimensionPanel } from './dimension_panel'; -import { IndexPatternDatasourceSetupPlugins } from './plugin'; import { IndexPatternDataPanel } from './datapanel'; import { getDatasourceSuggestionsForField, @@ -90,20 +89,16 @@ export function uniqueLabels(layers: Record) { } export function getIndexPatternDatasource({ - chrome, core, storage, - savedObjectsClient, data, -}: Pick & { - // Core start is being required here because it contains the savedObject client - // In the new platform, this plugin wouldn't be initialized until after setup +}: { core: CoreStart; storage: IStorageWrapper; - savedObjectsClient: SavedObjectsClientContract; data: ReturnType; }) { - const uiSettings = chrome.getUiSettingsClient(); + const savedObjectsClient = core.savedObjects.client; + const uiSettings = core.uiSettings; const onIndexPatternLoadError = (err: Error) => core.notifications.toasts.addError(err, { title: i18n.translate('xpack.lens.indexPattern.indexPatternLoadError', { @@ -118,7 +113,7 @@ export function getIndexPatternDatasource({ async initialize(state?: IndexPatternPersistedState) { return loadInitialState({ state, - savedObjectsClient, + savedObjectsClient: await savedObjectsClient, defaultIndexPatternId: core.uiSettings.get('defaultIndex'), }); }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts index 3ec4b4f4df2ce..ed3d8a91b366d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts @@ -5,7 +5,6 @@ */ import _ from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'src/core/public'; import { SimpleSavedObject } from 'src/core/public'; import { StateSetter } from '../types'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/mocks.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/mocks.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/mocks.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/__mocks__/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/__mocks__/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/cardinality.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/cardinality.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/column_types.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/column_types.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/metrics.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/metrics.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/to_expression.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/to_expression.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/utils.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/utils.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx deleted file mode 100644 index 11bc52fc48378..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CoreSetup } from 'src/core/public'; -// The following dependencies on ui/* and src/legacy/core_plugins must be mocked when testing -import chrome, { Chrome } from 'ui/chrome'; -import { npSetup, npStart } from 'ui/new_platform'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -import { getIndexPatternDatasource } from './indexpattern'; -import { renameColumns } from './rename_columns'; -import { autoDate } from './auto_date'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; - -// TODO these are intermediary types because interpreter is not typed yet -// They can get replaced by references to the real interfaces as soon as they -// are available - -export interface IndexPatternDatasourceSetupPlugins { - chrome: Chrome; - expressions: ExpressionsSetup; -} - -class IndexPatternDatasourcePlugin { - constructor() {} - - setup(core: CoreSetup, { expressions }: IndexPatternDatasourceSetupPlugins) { - expressions.registerFunction(renameColumns); - expressions.registerFunction(autoDate); - } - - stop() {} -} - -const plugin = new IndexPatternDatasourcePlugin(); - -export const indexPatternDatasourceSetup = () => { - plugin.setup(npSetup.core, { - chrome, - expressions: npSetup.plugins.expressions, - }); - - return getIndexPatternDatasource({ - core: npStart.core, - chrome, - storage: new Storage(localStorage), - savedObjectsClient: chrome.getSavedObjectsClient(), - data: npStart.plugins.data, - }); -}; -export const indexPatternDatasourceStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/legacy.ts b/x-pack/legacy/plugins/lens/public/legacy.ts index a39d73f187ece..8023bad34de66 100644 --- a/x-pack/legacy/plugins/lens/public/legacy.ts +++ b/x-pack/legacy/plugins/lens/public/legacy.ts @@ -5,15 +5,12 @@ */ import { npSetup, npStart } from 'ui/new_platform'; -import { start as dataShimStart } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { getFormat } from './legacy_imports'; export * from './types'; -import { AppPlugin } from './app_plugin'; +import { plugin } from './index'; -const app = new AppPlugin(); -app.setup(npSetup.core, npSetup.plugins); -app.start(npStart.core, { - ...npStart.plugins, - dataShim: dataShimStart, -}); +const pluginInstance = plugin(); +pluginInstance.setup(npSetup.core, { ...npSetup.plugins, __LEGACY: { formatFactory: getFormat } }); +pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts b/x-pack/legacy/plugins/lens/public/legacy_imports.ts similarity index 72% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts rename to x-pack/legacy/plugins/lens/public/legacy_imports.ts index f75dce9b7507f..9dcc22ddb1bb7 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts +++ b/x-pack/legacy/plugins/lens/public/legacy_imports.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.scss b/x-pack/legacy/plugins/lens/public/metric_visualization/index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.scss rename to x-pack/legacy/plugins/lens/public/metric_visualization/index.scss diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts new file mode 100644 index 0000000000000..217cc6902fc99 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/public'; +import { FormatFactory } from '../legacy_imports'; +import { metricVisualization } from './metric_visualization'; +import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { metricChart, getMetricChartRenderer } from './metric_expression'; +import { EditorFrameSetup } from '../types'; + +export interface MetricVisualizationPluginSetupPlugins { + expressions: ExpressionsSetup; + formatFactory: FormatFactory; + editorFrame: EditorFrameSetup; +} + +export class MetricVisualization { + constructor() {} + + setup( + _core: CoreSetup | null, + { expressions, formatFactory, editorFrame }: MetricVisualizationPluginSetupPlugins + ) { + expressions.registerFunction(() => metricChart); + + expressions.registerRenderer(() => getMetricChartRenderer(formatFactory)); + + editorFrame.registerVisualization(metricVisualization); + } +} diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx index a66239e5d30f6..eac35f82a50fa 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx @@ -11,7 +11,7 @@ import { MetricConfigPanel } from './metric_config_panel'; import { DatasourceDimensionPanelProps, Operation, DatasourcePublicAPI } from '../types'; import { State } from './types'; import { NativeRendererProps } from '../native_renderer'; -import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; describe('MetricConfigPanel', () => { const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts index c131612399cca..88964b95c2ac7 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts @@ -6,7 +6,7 @@ import { metricVisualization } from './metric_visualization'; import { State } from './types'; -import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; import { generateId } from '../id_generator'; import { DatasourcePublicAPI, FramePublicAPI } from '../types'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/types.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx deleted file mode 100644 index 219ef533a4ba3..0000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npSetup } from 'ui/new_platform'; -import { CoreSetup } from 'src/core/public'; -import { FormatFactory, getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; -import { metricVisualization } from './metric_visualization'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; -import { metricChart, getMetricChartRenderer } from './metric_expression'; - -export interface MetricVisualizationPluginSetupPlugins { - expressions: ExpressionsSetup; - // TODO this is a simulated NP plugin. - // Once field formatters are actually migrated, the actual shim can be used - fieldFormat: { - formatFactory: FormatFactory; - }; -} - -class MetricVisualizationPlugin { - constructor() {} - - setup( - _core: CoreSetup | null, - { expressions, fieldFormat }: MetricVisualizationPluginSetupPlugins - ) { - expressions.registerFunction(() => metricChart); - - expressions.registerRenderer(() => getMetricChartRenderer(fieldFormat.formatFactory)); - - return metricVisualization; - } - - stop() {} -} - -const plugin = new MetricVisualizationPlugin(); - -export const metricVisualizationSetup = () => - plugin.setup(null, { - expressions: npSetup.plugins.expressions, - fieldFormat: { - formatFactory: getFormat, - }, - }); - -export const metricVisualizationStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx index 012c27d3ce3ff..38f48c9cdaf72 100644 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx +++ b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockDatasource } from '../editor_frame_service/mocks'; import { MultiColumnEditor } from './multi_column_editor'; import { mount } from 'enzyme'; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx similarity index 55% rename from x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx rename to x-pack/legacy/plugins/lens/public/plugin.tsx index 283f4d2a0689d..634d227559835 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -4,97 +4,112 @@ * you may not use this file except in compliance with the Elastic License. */ -import 'ui/autoload/all'; -// Used to run esaggs queries -import 'uiExports/fieldFormats'; -import 'uiExports/search'; -import 'uiExports/visRequestHandlers'; -import 'uiExports/visResponseHandlers'; -// Used for kibana_context function -import 'uiExports/savedObjectTypes'; - import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { render, unmountComponentAtNode } from 'react-dom'; -import { CoreSetup, CoreStart, SavedObjectsClientContract } from 'src/core/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { AppMountParameters, CoreSetup, CoreStart } from 'src/core/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; import rison, { RisonObject, RisonValue } from 'rison-node'; import { isObject } from 'lodash'; -import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -import { editorFrameSetup, editorFrameStart, editorFrameStop } from '../editor_frame_plugin'; -import { indexPatternDatasourceSetup, indexPatternDatasourceStop } from '../indexpattern_plugin'; -import { addHelpMenuToAppChrome } from '../help_menu_util'; -import { SavedObjectIndexStore } from '../persistence'; -import { xyVisualizationSetup, xyVisualizationStop } from '../xy_visualization_plugin'; -import { metricVisualizationSetup, metricVisualizationStop } from '../metric_visualization_plugin'; -import { - datatableVisualizationSetup, - datatableVisualizationStop, -} from '../datatable_visualization_plugin'; -import { App } from './app'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { EditorFrameService } from './editor_frame_service'; +import { IndexPatternDatasource } from './indexpattern_datasource'; +import { addHelpMenuToAppChrome } from './help_menu_util'; +import { SavedObjectIndexStore } from './persistence'; +import { XyVisualization } from './xy_visualization'; +import { MetricVisualization } from './metric_visualization'; +import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; +import { DatatableVisualization } from './datatable_visualization'; +import { App } from './app_plugin'; import { LensReportManager, setReportManager, stopReportManager, trackUiEvent, -} from '../lens_ui_telemetry'; -import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../../../plugins/lens/common'; -import { KibanaLegacySetup } from '../../../../../../src/plugins/kibana_legacy/public'; -import { EditorFrameStart } from '../types'; +} from './lens_ui_telemetry'; +import { KibanaLegacySetup } from '../../../../../src/plugins/kibana_legacy/public'; +import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../../plugins/lens/common'; import { addEmbeddableToDashboardUrl, getUrlVars, getLensUrlFromDashboardAbsoluteUrl, -} from '../../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; +} from '../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; +import { FormatFactory } from './legacy_imports'; +import { IEmbeddableSetup, IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { EditorFrameStart } from './types'; export interface LensPluginSetupDependencies { kibanaLegacy: KibanaLegacySetup; + expressions: ExpressionsSetup; + data: DataPublicPluginSetup; + embeddable: IEmbeddableSetup; + __LEGACY: { + formatFactory: FormatFactory; + }; } export interface LensPluginStartDependencies { data: DataPublicPluginStart; - dataShim: DataStart; + embeddable: IEmbeddableStart; + expressions: ExpressionsStart; } export const isRisonObject = (value: RisonValue): value is RisonObject => { return isObject(value); }; -export class AppPlugin { - private startDependencies: { - data: DataPublicPluginStart; - dataShim: DataStart; - savedObjectsClient: SavedObjectsClientContract; - editorFrame: EditorFrameStart; - } | null = null; - - constructor() {} - - setup(core: CoreSetup, { kibanaLegacy }: LensPluginSetupDependencies) { - // TODO: These plugins should not be called from the top level, but since this is the - // entry point to the app we have no choice until the new platform is ready - const indexPattern = indexPatternDatasourceSetup(); - const datatableVisualization = datatableVisualizationSetup(); - const xyVisualization = xyVisualizationSetup(); - const metricVisualization = metricVisualizationSetup(); - const editorFrameSetupInterface = editorFrameSetup(); +export class LensPlugin { + private datatableVisualization: DatatableVisualization; + private editorFrameService: EditorFrameService; + private createEditorFrame: EditorFrameStart['createInstance'] | null = null; + private indexpatternDatasource: IndexPatternDatasource; + private xyVisualization: XyVisualization; + private metricVisualization: MetricVisualization; + + constructor() { + this.datatableVisualization = new DatatableVisualization(); + this.editorFrameService = new EditorFrameService(); + this.indexpatternDatasource = new IndexPatternDatasource(); + this.xyVisualization = new XyVisualization(); + this.metricVisualization = new MetricVisualization(); + } - editorFrameSetupInterface.registerVisualization(xyVisualization); - editorFrameSetupInterface.registerVisualization(datatableVisualization); - editorFrameSetupInterface.registerVisualization(metricVisualization); - editorFrameSetupInterface.registerDatasource(indexPattern); + setup( + core: CoreSetup, + { + kibanaLegacy, + expressions, + data, + embeddable, + __LEGACY: { formatFactory }, + }: LensPluginSetupDependencies + ) { + const editorFrameSetupInterface = this.editorFrameService.setup(core, { + data, + embeddable, + expressions, + }); + const dependencies = { + expressions, + data, + editorFrame: editorFrameSetupInterface, + formatFactory, + }; + this.indexpatternDatasource.setup(core, dependencies); + this.xyVisualization.setup(core, dependencies); + this.datatableVisualization.setup(core, dependencies); + this.metricVisualization.setup(core, dependencies); kibanaLegacy.registerLegacyApp({ id: 'lens', title: NOT_INTERNATIONALIZED_PRODUCT_NAME, - mount: async (context, params) => { - if (this.startDependencies === null) { - throw new Error('mounted before start phase'); - } - const { data, savedObjectsClient, editorFrame } = this.startDependencies; - addHelpMenuToAppChrome(context.core.chrome); - const instance = editorFrame.createInstance({}); + mount: async (params: AppMountParameters) => { + const [coreStart, startDependencies] = await core.getStartServices(); + const dataStart = startDependencies.data; + const savedObjectsClient = coreStart.savedObjects.client; + addHelpMenuToAppChrome(coreStart.chrome); + + const instance = await this.createEditorFrame!({}); setReportManager( new LensReportManager({ @@ -108,7 +123,7 @@ export class AppPlugin { return; } // @ts-ignore - decoded.time = data.query.timefilter.timefilter.getTime(); + decoded.time = dataStart.query.timefilter.timefilter.getTime(); urlVars._g = rison.encode(decoded); }; const redirectTo = ( @@ -122,12 +137,12 @@ export class AppPlugin { routeProps.history.push(`/lens/edit/${id}`); } else if (addToDashboardMode && id) { routeProps.history.push(`/lens/edit/${id}`); - const url = context.core.chrome.navLinks.get('kibana:dashboard'); + const url = coreStart.chrome.navLinks.get('kibana:dashboard'); if (!url) { throw new Error('Cannot get last dashboard url'); } const lastDashboardAbsoluteUrl = url.url; - const basePath = context.core.http.basePath.get(); + const basePath = coreStart.http.basePath.get(); const lensUrl = getLensUrlFromDashboardAbsoluteUrl( lastDashboardAbsoluteUrl, basePath, @@ -158,8 +173,8 @@ export class AppPlugin { !!routeProps.location.search && routeProps.location.search.includes('addToDashboard'); return ( (datasource: Datasource) => void; - registerVisualization: (visualization: Visualization) => void; + registerDatasource: (datasource: Datasource | Promise>) => void; + registerVisualization: ( + visualization: Visualization | Promise> + ) => void; } export interface EditorFrameStart { - createInstance: (options: EditorFrameOptions) => EditorFrameInstance; + createInstance: (options: EditorFrameOptions) => Promise; } // Hints the default nesting to the data source. 0 is the highest priority diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap new file mode 100644 index 0000000000000..fd0c4b8212fc6 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap @@ -0,0 +1,482 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`xy_expression XYChart component it renders area 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders bar 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders line 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders stacked area 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders stacked bar 1`] = ` + + + + + + +`; + +exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = ` + + + + + + +`; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap rename to x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_expression.scss b/x-pack/legacy/plugins/lens/public/xy_visualization/_xy_expression.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_expression.scss rename to x-pack/legacy/plugins/lens/public/xy_visualization/_xy_expression.scss diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/index.ts similarity index 57% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/index.ts index 6feece99370ef..86c52e0577616 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/index.ts @@ -4,24 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { npSetup } from 'ui/new_platform'; +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { CoreSetup, IUiSettingsClient } from 'src/core/public'; -import chrome, { Chrome } from 'ui/chrome'; import moment from 'moment-timezone'; -import { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; +import { FormatFactory } from '../legacy_imports'; import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; import { xyVisualization } from './xy_visualization'; import { xyChart, getXyChartRenderer } from './xy_expression'; import { legendConfig, xConfig, layerConfig } from './types'; +import { EditorFrameSetup } from '../types'; export interface XyVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; - chrome: Chrome; - // TODO this is a simulated NP plugin. - // Once field formatters are actually migrated, the actual shim can be used - fieldFormat: { - formatFactory: FormatFactory; - }; + formatFactory: FormatFactory; + editorFrame: EditorFrameSetup; } function getTimeZone(uiSettings: IUiSettingsClient) { @@ -33,16 +29,12 @@ function getTimeZone(uiSettings: IUiSettingsClient) { return configuredTimeZone; } -class XyVisualizationPlugin { +export class XyVisualization { constructor() {} setup( - _core: CoreSetup | null, - { - expressions, - fieldFormat: { formatFactory }, - chrome: { getUiSettingsClient }, - }: XyVisualizationPluginSetupPlugins + core: CoreSetup, + { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins ) { expressions.registerFunction(() => legendConfig); expressions.registerFunction(() => xConfig); @@ -52,24 +44,13 @@ class XyVisualizationPlugin { expressions.registerRenderer( getXyChartRenderer({ formatFactory, - timeZone: getTimeZone(getUiSettingsClient()), + chartTheme: core.uiSettings.get('theme:darkMode') + ? EUI_CHARTS_THEME_DARK.theme + : EUI_CHARTS_THEME_LIGHT.theme, + timeZone: getTimeZone(core.uiSettings), }) ); - return xyVisualization; + editorFrame.registerVisualization(xyVisualization); } - - stop() {} } - -const plugin = new XyVisualizationPlugin(); - -export const xyVisualizationSetup = () => - plugin.setup(null, { - expressions: npSetup.plugins.expressions, - fieldFormat: { - formatFactory: getFormat, - }, - chrome, - }); -export const xyVisualizationStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/state_helpers.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/types.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 6ed827bc71c68..301c4a58a0ffd 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -14,7 +14,7 @@ import { State } from './types'; import { Position } from '@elastic/charts'; import { NativeRendererProps } from '../native_renderer'; import { generateId } from '../id_generator'; -import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; jest.mock('../id_generator'); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx similarity index 93% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx index daedb30db3f3e..04e0b80faa200 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -132,6 +132,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'line' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -156,6 +157,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` @@ -184,6 +186,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(Settings).prop('xDomain')).toBeUndefined(); @@ -197,6 +200,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -211,6 +215,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -225,6 +230,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_horizontal' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -240,6 +246,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -255,6 +262,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area_stacked' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -273,6 +281,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -284,7 +293,13 @@ describe('xy_expression', () => { test('it passes time zone to the series', () => { const { data, args } = sampleArgs(); const component = shallow( - + ); expect(component.find(LineSeries).prop('timeZone')).toEqual('CEST'); }); @@ -299,6 +314,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [firstLayer] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); @@ -321,6 +337,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); @@ -337,6 +354,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(false); @@ -346,7 +364,13 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( - + ); expect(component.find(LineSeries).prop('data')).toEqual([ { 'Label A': 1, 'Label B': 2, c: 'I', 'Label D': 'Foo', d: 'Foo' }, @@ -358,7 +382,13 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( - + ); expect(component.find(LineSeries).prop('yAccessors')).toEqual(['Label A', 'Label B']); }); @@ -372,6 +402,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], xScaleType: 'ordinal' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Ordinal); @@ -386,6 +417,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], yScaleType: 'sqrt' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(LineSeries).prop('yScaleType')).toEqual(ScaleType.Sqrt); @@ -400,6 +432,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); @@ -415,6 +448,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); @@ -429,6 +463,7 @@ describe('xy_expression', () => { data={{ ...data }} args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }} formatFactory={getFormatSpy} + chartTheme={{}} timeZone="UTC" /> ); @@ -447,6 +482,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx index c62a8288d6655..27fd6e7064042 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -6,7 +6,6 @@ import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; -import chrome from 'ui/chrome'; import { Chart, Settings, @@ -15,6 +14,7 @@ import { AreaSeries, BarSeries, Position, + PartialTheme, } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { @@ -27,16 +27,12 @@ import { import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -import { FormatFactory } from '../../../../../../src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities'; +import { FormatFactory } from '../legacy_imports'; import { LensMultiTable } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; -const IS_DARK_THEME = chrome.getUiSettingsClient().get('theme:darkMode'); -const chartTheme = IS_DARK_THEME ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - export interface XYChartProps { data: LensMultiTable; args: XYArgs; @@ -49,6 +45,7 @@ export interface XYRender { } type XYChartRenderProps = XYChartProps & { + chartTheme: PartialTheme; formatFactory: FormatFactory; timeZone: string; }; @@ -101,6 +98,7 @@ export const xyChart: ExpressionFunctionDefinition< export const getXyChartRenderer = (dependencies: { formatFactory: FormatFactory; + chartTheme: PartialTheme; timeZone: string; }): ExpressionRenderDefinition => ({ name: 'lens_xy_chart_renderer', @@ -146,7 +144,7 @@ export function XYChartReportable(props: XYChartRenderProps) { ); } -export function XYChart({ data, args, formatFactory, timeZone }: XYChartRenderProps) { +export function XYChart({ data, args, formatFactory, timeZone, chartTheme }: XYChartRenderProps) { const { legend, layers } = args; if (Object.values(data.tables).every(table => table.rows.length === 0)) { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts index 89794ec74eaec..a27a8e7754b86 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -8,7 +8,7 @@ import { xyVisualization } from './xy_visualization'; import { Position } from '@elastic/charts'; import { Operation } from '../types'; import { State, SeriesType } from './types'; -import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; import { generateId } from '../id_generator'; import { Ast } from '@kbn/interpreter/target/common'; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap deleted file mode 100644 index 495d7a7bcd77e..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ /dev/null @@ -1,1315 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`xy_expression XYChart component it renders area 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders bar 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders line 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders stacked area 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders stacked bar 1`] = ` - - - - - - -`; - -exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = ` - - - - - - -`; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.ts deleted file mode 100644 index f75dce9b7507f..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './plugin'; diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js index 59b54c2434d17..2c6c60db9a012 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import turf from 'turf'; import turfBooleanContains from '@turf/boolean-contains'; +import uuid from 'uuid/v4'; import { getLayerList, getLayerListRaw, @@ -14,7 +16,7 @@ import { getMapReady, getWaitingForMapReadyLayerListRaw, getTransientLayerId, - getTooltipState, + getOpenTooltips, getQuery, } from '../selectors/map_selectors'; import { FLYOUT_STATE } from '../reducers/ui'; @@ -63,7 +65,7 @@ export const CLEAR_GOTO = 'CLEAR_GOTO'; export const TRACK_CURRENT_LAYER_STATE = 'TRACK_CURRENT_LAYER_STATE'; export const ROLLBACK_TO_TRACKED_LAYER_STATE = 'ROLLBACK_TO_TRACKED_LAYER_STATE'; export const REMOVE_TRACKED_LAYER_STATE = 'REMOVE_TRACKED_LAYER_STATE'; -export const SET_TOOLTIP_STATE = 'SET_TOOLTIP_STATE'; +export const SET_OPEN_TOOLTIPS = 'SET_OPEN_TOOLTIPS'; export const UPDATE_DRAW_STATE = 'UPDATE_DRAW_STATE'; export const SET_SCROLL_ZOOM = 'SET_SCROLL_ZOOM'; export const SET_MAP_INIT_ERROR = 'SET_MAP_INIT_ERROR'; @@ -221,34 +223,36 @@ function setLayerDataLoadErrorStatus(layerId, errorMessage) { export function cleanTooltipStateForLayer(layerId, layerFeatures = []) { return (dispatch, getState) => { - const tooltipState = getTooltipState(getState()); - - if (!tooltipState) { - return; - } - - const nextTooltipFeatures = tooltipState.features.filter(tooltipFeature => { - if (tooltipFeature.layerId !== layerId) { - // feature from another layer, keep it - return true; - } - - // Keep feature if it is still in layer - return layerFeatures.some(layerFeature => { - return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; + let featuresRemoved = false; + const openTooltips = getOpenTooltips(getState()) + .map(tooltipState => { + const nextFeatures = tooltipState.features.filter(tooltipFeature => { + if (tooltipFeature.layerId !== layerId) { + // feature from another layer, keep it + return true; + } + + // Keep feature if it is still in layer + return layerFeatures.some(layerFeature => { + return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; + }); + }); + + if (tooltipState.features.length !== nextFeatures.length) { + featuresRemoved = true; + } + + return { ...tooltipState, features: nextFeatures }; + }) + .filter(tooltipState => { + return tooltipState.features.length > 0; }); - }); - - if (tooltipState.features.length === nextTooltipFeatures.length) { - // no features got removed, nothing to update - return; - } - if (nextTooltipFeatures.length === 0) { - // all features removed from tooltip, close tooltip - dispatch(setTooltipState(null)); - } else { - dispatch(setTooltipState({ ...tooltipState, features: nextTooltipFeatures })); + if (featuresRemoved) { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); } }; } @@ -412,10 +416,61 @@ export function mapExtentChanged(newMapConstants) { }; } -export function setTooltipState(tooltipState) { +export function closeOnClickTooltip(tooltipId) { + return (dispatch, getState) => { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips: getOpenTooltips(getState()).filter(({ id }) => { + return tooltipId !== id; + }), + }); + }; +} + +export function openOnClickTooltip(tooltipState) { + return (dispatch, getState) => { + const openTooltips = getOpenTooltips(getState()).filter(({ features, location, isLocked }) => { + return ( + isLocked && + !_.isEqual(location, tooltipState.location) && + !_.isEqual(features, tooltipState.features) + ); + }); + + openTooltips.push({ + ...tooltipState, + isLocked: true, + id: uuid(), + }); + + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); + }; +} + +export function closeOnHoverTooltip() { + return (dispatch, getState) => { + if (getOpenTooltips(getState()).length) { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips: [], + }); + } + }; +} + +export function openOnHoverTooltip(tooltipState) { return { - type: 'SET_TOOLTIP_STATE', - tooltipState: tooltipState, + type: SET_OPEN_TOOLTIPS, + openTooltips: [ + { + ...tooltipState, + isLocked: false, + id: uuid(), + }, + ], }; } @@ -826,9 +881,9 @@ export function setJoinsForLayer(layer, joins) { } export function updateDrawState(drawState) { - return async dispatch => { + return dispatch => { if (drawState !== null) { - await dispatch(setTooltipState(null)); //tooltips just get in the way + dispatch({ type: SET_OPEN_TOOLTIPS, openTooltips: [] }); // tooltips just get in the way } dispatch({ type: UPDATE_DRAW_STATE, diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js index 0274f849daf3d..9148fbdfd2d1e 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js @@ -16,7 +16,6 @@ import { setMapInitError, } from '../../../actions/map_actions'; import { - getTooltipState, getLayerList, getMapReady, getGoto, @@ -33,7 +32,6 @@ function mapStateToProps(state = {}) { layerList: getLayerList(state), goto: getGoto(state), inspectorAdapters: getInspectorAdapters(state), - tooltipState: getTooltipState(state), scrollZoom: getScrollZoom(state), disableInteractive: isInteractiveDisabled(state), disableTooltipControl: isTooltipControlDisabled(state), diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap index 7e8feeec01bbd..cffa441d04ff5 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap @@ -1,117 +1,97 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TooltipControl render tooltipState is not provided should not render tooltip popover when tooltipState is not provided 1`] = `""`; +exports[`TooltipControl render should not render tooltips when there are no open tooltips 1`] = `""`; -exports[`TooltipControl render tooltipState is provided should render tooltip popover with custom tooltip content when renderTooltipContent provided 1`] = ` - +exports[`TooltipControl render should render hover tooltip 1`] = ` + -
- Custom tooltip content -
-
+/> `; -exports[`TooltipControl render tooltipState is provided should render tooltip popover with features tooltip content 1`] = ` - +exports[`TooltipControl render should render locked tooltip 1`] = ` + - - - - + } +/> `; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap new file mode 100644 index 0000000000000..d95a418988ae7 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TooltipPopover render should render tooltip popover 1`] = ` + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="mapTooltip" + isOpen={true} + ownFocus={false} + panelPaddingSize="m" + style={ + Object { + "pointerEvents": "none", + "transform": "translate(NaNpx, 2987px)", + } + } +> + + + + +`; + +exports[`TooltipPopover render should render tooltip popover with custom tooltip content when renderTooltipContent provided 1`] = ` + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="mapTooltip" + isOpen={true} + ownFocus={false} + panelPaddingSize="m" + style={ + Object { + "pointerEvents": "none", + "transform": "translate(NaNpx, 2987px)", + } + } +> +
+ Custom tooltip content +
+
+`; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js index 6bc9511c6c580..d3cdbfeca3e57 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js @@ -6,28 +6,41 @@ import { connect } from 'react-redux'; import { TooltipControl } from './tooltip_control'; -import { setTooltipState } from '../../../../actions/map_actions'; +import { + closeOnClickTooltip, + openOnClickTooltip, + closeOnHoverTooltip, + openOnHoverTooltip, +} from '../../../../actions/map_actions'; import { getLayerList, - getTooltipState, + getOpenTooltips, + getHasLockedTooltips, isDrawingFilter, } from '../../../../selectors/map_selectors'; function mapStateToProps(state = {}) { return { layerList: getLayerList(state), - tooltipState: getTooltipState(state), + hasLockedTooltips: getHasLockedTooltips(state), isDrawingFilter: isDrawingFilter(state), + openTooltips: getOpenTooltips(state), }; } function mapDispatchToProps(dispatch) { return { - setTooltipState(tooltipState) { - dispatch(setTooltipState(tooltipState)); + closeOnClickTooltip(tooltipId) { + dispatch(closeOnClickTooltip(tooltipId)); + }, + openOnClickTooltip(tooltipState) { + dispatch(openOnClickTooltip(tooltipState)); + }, + closeOnHoverTooltip() { + dispatch(closeOnHoverTooltip()); }, - clearTooltipState() { - dispatch(setTooltipState(null)); + openOnHoverTooltip(tooltipState) { + dispatch(openOnHoverTooltip(tooltipState)); }, }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js index cfb92a8677455..329d2b7fd2985 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js @@ -6,16 +6,8 @@ import _ from 'lodash'; import React from 'react'; -import { FEATURE_ID_PROPERTY_NAME, LAT_INDEX, LON_INDEX } from '../../../../../common/constants'; -import { FeaturesTooltip } from '../../features_tooltip/features_tooltip'; -import { EuiPopover, EuiText } from '@elastic/eui'; - -export const TOOLTIP_TYPE = { - HOVER: 'HOVER', - LOCKED: 'LOCKED', -}; - -const noop = () => {}; +import { FEATURE_ID_PROPERTY_NAME, LON_INDEX } from '../../../../../common/constants'; +import { TooltipPopover } from './tooltip_popover'; function justifyAnchorLocation(mbLngLat, targetFeature) { let popupAnchorLocation = [mbLngLat.lng, mbLngLat.lat]; // default popup location to mouse location @@ -35,80 +27,23 @@ function justifyAnchorLocation(mbLngLat, targetFeature) { } export class TooltipControl extends React.Component { - state = { - x: undefined, - y: undefined, - }; - - constructor(props) { - super(props); - this._popoverRef = React.createRef(); - } - - static getDerivedStateFromProps(nextProps, prevState) { - if (nextProps.tooltipState) { - const nextPoint = nextProps.mbMap.project(nextProps.tooltipState.location); - if (nextPoint.x !== prevState.x || nextPoint.y !== prevState.y) { - return { - x: nextPoint.x, - y: nextPoint.y, - }; - } - } - - return null; - } - componentDidMount() { this.props.mbMap.on('mouseout', this._onMouseout); this.props.mbMap.on('mousemove', this._updateHoverTooltipState); - this.props.mbMap.on('move', this._updatePopoverPosition); this.props.mbMap.on('click', this._lockTooltip); } - componentDidUpdate() { - if (this.props.tooltipState && this._popoverRef.current) { - this._popoverRef.current.positionPopoverFluid(); - } - } - componentWillUnmount() { this.props.mbMap.off('mouseout', this._onMouseout); this.props.mbMap.off('mousemove', this._updateHoverTooltipState); - this.props.mbMap.off('move', this._updatePopoverPosition); this.props.mbMap.off('click', this._lockTooltip); } _onMouseout = () => { this._updateHoverTooltipState.cancel(); - if (this.props.tooltipState && this.props.tooltipState.type !== TOOLTIP_TYPE.LOCKED) { - this.props.clearTooltipState(); - } - }; - - _updatePopoverPosition = () => { - if (!this.props.tooltipState) { - return; + if (!this.props.hasLockedTooltips) { + this.props.closeOnHoverTooltip(); } - - const lat = this.props.tooltipState.location[LAT_INDEX]; - const lon = this.props.tooltipState.location[LON_INDEX]; - const bounds = this.props.mbMap.getBounds(); - if ( - lat > bounds.getNorth() || - lat < bounds.getSouth() || - lon < bounds.getWest() || - lon > bounds.getEast() - ) { - this.props.clearTooltipState(); - return; - } - - const nextPoint = this.props.mbMap.project(this.props.tooltipState.location); - this.setState({ - x: nextPoint.x, - y: nextPoint.y, - }); }; _getLayerByMbLayerId(mbLayerId) { @@ -148,7 +83,7 @@ export class TooltipControl extends React.Component { _lockTooltip = e => { if (this.props.isDrawingFilter) { - //ignore click events when in draw mode + // ignore click events when in draw mode return; } @@ -156,7 +91,7 @@ export class TooltipControl extends React.Component { const mbFeatures = this._getFeaturesUnderPointer(e.point); if (!mbFeatures.length) { - this.props.clearTooltipState(); + // No features at click location so there is no tooltip to open return; } @@ -164,42 +99,36 @@ export class TooltipControl extends React.Component { const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeataure); const features = this._getIdsForFeatures(mbFeatures); - this.props.setTooltipState({ - type: TOOLTIP_TYPE.LOCKED, + this.props.openOnClickTooltip({ features: features, location: popupAnchorLocation, }); }; _updateHoverTooltipState = _.debounce(e => { - if (this.props.isDrawingFilter) { - //ignore hover events when in draw mode - return; - } - - if (this.props.tooltipState && this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED) { - //ignore hover events when tooltip is locked + if (this.props.isDrawingFilter || this.props.hasLockedTooltips) { + // ignore hover events when in draw mode or when there are locked tooltips return; } const mbFeatures = this._getFeaturesUnderPointer(e.point); if (!mbFeatures.length) { - this.props.clearTooltipState(); + this.props.closeOnHoverTooltip(); return; } const targetMbFeature = mbFeatures[0]; - if (this.props.tooltipState) { - const firstFeature = this.props.tooltipState.features[0]; + if (this.props.openTooltips[0]) { + const firstFeature = this.props.openTooltips[0].features[0]; if (targetMbFeature.properties[FEATURE_ID_PROPERTY_NAME] === firstFeature.id) { + // ignore hover events when hover tooltip is all ready opened for feature return; } } const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeature); const features = this._getIdsForFeatures(mbFeatures); - this.props.setTooltipState({ - type: TOOLTIP_TYPE.HOVER, + this.props.openOnHoverTooltip({ features: features, location: popupAnchorLocation, }); @@ -240,114 +169,32 @@ export class TooltipControl extends React.Component { return this.props.mbMap.queryRenderedFeatures(mbBbox, { layers: mbLayerIds }); } - // Must load original geometry instead of using geometry from mapbox feature. - // Mapbox feature geometry is from vector tile and is not the same as the original geometry. - _loadFeatureGeometry = ({ layerId, featureId }) => { - const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { - return null; - } - - const targetFeature = tooltipLayer.getFeatureById(featureId); - if (!targetFeature) { - return null; - } - - return targetFeature.geometry; - }; - - _loadFeatureProperties = async ({ layerId, featureId }) => { - const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { - return []; - } - - const targetFeature = tooltipLayer.getFeatureById(featureId); - if (!targetFeature) { - return []; - } - return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties); - }; - - _loadPreIndexedShape = async ({ layerId, featureId }) => { - const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { - return null; - } - - const targetFeature = tooltipLayer.getFeatureById(featureId); - if (!targetFeature) { - return null; - } - - return await tooltipLayer.getSource().getPreIndexedShape(targetFeature.properties); - }; - - _findLayerById = layerId => { - return this.props.layerList.find(layer => { - return layer.getId() === layerId; - }); - }; - - _getLayerName = async layerId => { - const layer = this._findLayerById(layerId); - if (!layer) { + render() { + if (this.props.openTooltips.length === 0) { return null; } - return layer.getDisplayName(); - }; - - _renderTooltipContent = () => { - const publicProps = { - addFilters: this.props.addFilters, - closeTooltip: this.props.clearTooltipState, - features: this.props.tooltipState.features, - isLocked: this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED, - loadFeatureProperties: this._loadFeatureProperties, - loadFeatureGeometry: this._loadFeatureGeometry, - getLayerName: this._getLayerName, - }; - - if (this.props.renderTooltipContent) { - return this.props.renderTooltipContent(publicProps); - } - - return ( - - { + const closeTooltip = isLocked + ? () => { + this.props.closeOnClickTooltip(id); + } + : this.props.closeOnHoverTooltip; + return ( + - - ); - }; - - render() { - if (!this.props.tooltipState) { - return null; - } - - const tooltipAnchor = ( -
- ); - return ( - - {this._renderTooltipContent()} - - ); + ); + }); } } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js index b9dc668cfb016..620d7cb9ff756 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js @@ -4,21 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../features_tooltip/features_tooltip', () => ({ - FeaturesTooltip: () => { - return
mockFeaturesTooltip
; +jest.mock('./tooltip_popover', () => ({ + TooltipPopover: () => { + return
mockTooltipPopover
; }, })); import sinon from 'sinon'; import React from 'react'; import { mount, shallow } from 'enzyme'; -import { TooltipControl, TOOLTIP_TYPE } from './tooltip_control'; +import { TooltipControl } from './tooltip_control'; // mutable map state let featuresAtLocation; -let mapCenter; -let mockMbMapBounds; const layerId = 'tfi3f'; const mbLayerId = 'tfi3f_circle'; @@ -32,48 +30,16 @@ const mockLayer = { canShowTooltip: () => { return true; }, - getFeatureById: () => { - return { - geometry: { - type: 'Point', - coordinates: [102.0, 0.5], - }, - }; - }, }; const mockMbMapHandlers = {}; const mockMBMap = { - project: lonLatArray => { - const lonDistanceFromCenter = Math.abs(lonLatArray[0] - mapCenter[0]); - const latDistanceFromCenter = Math.abs(lonLatArray[1] - mapCenter[1]); - return { - x: lonDistanceFromCenter * 100, - y: latDistanceFromCenter * 100, - }; - }, on: (eventName, callback) => { mockMbMapHandlers[eventName] = callback; }, off: eventName => { delete mockMbMapHandlers[eventName]; }, - getBounds: () => { - return { - getNorth: () => { - return mockMbMapBounds.north; - }, - getSouth: () => { - return mockMbMapBounds.south; - }, - getWest: () => { - return mockMbMapBounds.west; - }, - getEast: () => { - return mockMbMapBounds.east; - }, - }; - }, getLayer: () => {}, queryRenderedFeatures: () => { return featuresAtLocation; @@ -82,16 +48,21 @@ const mockMBMap = { const defaultProps = { mbMap: mockMBMap, - clearTooltipState: () => {}, - setTooltipState: () => {}, + closeOnClickTooltip: () => {}, + openOnClickTooltip: () => {}, + closeOnHoverTooltip: () => {}, + openOnHoverTooltip: () => {}, layerList: [mockLayer], isDrawingFilter: false, addFilters: () => {}, geoFields: [{}], + openTooltips: [], + hasLockedTooltips: false, }; const hoverTooltipState = { - type: TOOLTIP_TYPE.HOVER, + id: '1', + isLocked: false, location: [-120, 30], features: [ { @@ -103,7 +74,8 @@ const hoverTooltipState = { }; const lockedTooltipState = { - type: TOOLTIP_TYPE.LOCKED, + id: '2', + isLocked: true, location: [-120, 30], features: [ { @@ -117,82 +89,79 @@ const lockedTooltipState = { describe('TooltipControl', () => { beforeEach(() => { featuresAtLocation = []; - mapCenter = [0, 0]; - mockMbMapBounds = { - west: -180, - east: 180, - north: 90, - south: -90, - }; }); describe('render', () => { - describe('tooltipState is not provided', () => { - test('should not render tooltip popover when tooltipState is not provided', () => { - const component = shallow(); + test('should not render tooltips when there are no open tooltips', () => { + const component = shallow(); - expect(component).toMatchSnapshot(); - }); + expect(component).toMatchSnapshot(); }); - describe('tooltipState is provided', () => { - test('should render tooltip popover with features tooltip content', () => { - const component = shallow( - - ); + test('should render hover tooltip', () => { + const component = shallow( + + ); - expect(component).toMatchSnapshot(); - }); + expect(component).toMatchSnapshot(); + }); - test('should render tooltip popover with custom tooltip content when renderTooltipContent provided', () => { - const component = shallow( - { - return
Custom tooltip content
; - }} - /> - ); - - expect(component).toMatchSnapshot(); - }); + test('should render locked tooltip', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('should un-register all map callbacks on unmount', () => { + const component = mount(); + + expect(Object.keys(mockMbMapHandlers).length).toBe(3); + + component.unmount(); + expect(Object.keys(mockMbMapHandlers).length).toBe(0); }); }); describe('on mouse out', () => { - const clearTooltipStateStub = sinon.stub(); + const closeOnHoverTooltipStub = sinon.stub(); beforeEach(() => { - clearTooltipStateStub.reset(); + closeOnHoverTooltipStub.reset(); }); test('should clear hover tooltip state', () => { mount( ); mockMbMapHandlers.mouseout(); - sinon.assert.calledOnce(clearTooltipStateStub); + sinon.assert.calledOnce(closeOnHoverTooltipStub); }); test('should not clear locked tooltip state', () => { mount( ); mockMbMapHandlers.mouseout(); - sinon.assert.notCalled(clearTooltipStateStub); + sinon.assert.notCalled(closeOnHoverTooltipStub); }); }); @@ -201,44 +170,44 @@ describe('TooltipControl', () => { point: { x: 0, y: 0 }, lngLat: { lng: 0, lat: 0 }, }; - const setTooltipStateStub = sinon.stub(); - const clearTooltipStateStub = sinon.stub(); + const openOnClickTooltipStub = sinon.stub(); + const closeOnClickTooltipStub = sinon.stub(); beforeEach(() => { - setTooltipStateStub.reset(); - clearTooltipStateStub.reset(); + openOnClickTooltipStub.reset(); + closeOnClickTooltipStub.reset(); }); test('should ignore clicks when map is in drawing mode', () => { mount( ); mockMbMapHandlers.click(mockMapMouseEvent); - sinon.assert.notCalled(clearTooltipStateStub); - sinon.assert.notCalled(setTooltipStateStub); + sinon.assert.notCalled(closeOnClickTooltipStub); + sinon.assert.notCalled(openOnClickTooltipStub); }); - test('should clear tooltip state when there are no features at clicked location', () => { + test('should not open tooltip when there are no features at clicked location', () => { featuresAtLocation = []; mount( ); mockMbMapHandlers.click(mockMapMouseEvent); - sinon.assert.calledOnce(clearTooltipStateStub); - sinon.assert.notCalled(setTooltipStateStub); + sinon.assert.notCalled(closeOnClickTooltipStub); + sinon.assert.notCalled(openOnClickTooltipStub); }); test('should set tooltip state when there are features at clicked location and remove duplicate features', () => { @@ -258,93 +227,18 @@ describe('TooltipControl', () => { mount( ); mockMbMapHandlers.click(mockMapMouseEvent); - sinon.assert.notCalled(clearTooltipStateStub); - sinon.assert.calledWith(setTooltipStateStub, { + sinon.assert.notCalled(closeOnClickTooltipStub); + sinon.assert.calledWith(openOnClickTooltipStub, { features: [{ id: 1, layerId: 'tfi3f' }], location: [100, 30], - type: 'LOCKED', }); }); }); - - describe('on map move', () => { - const clearTooltipStateStub = sinon.stub(); - - beforeEach(() => { - clearTooltipStateStub.reset(); - }); - - test('should safely handle map move when there is no tooltip location', () => { - const component = mount( - - ); - - mockMbMapHandlers.move(); - component.update(); - - sinon.assert.notCalled(clearTooltipStateStub); - }); - - test('should update popover location', () => { - const component = mount( - - ); - - // ensure x and y set from original tooltipState.location - expect(component.state('x')).toBe(12000); - expect(component.state('y')).toBe(3000); - - mapCenter = [25, -15]; - mockMbMapHandlers.move(); - component.update(); - - // ensure x and y updated from new map center with same tooltipState.location - expect(component.state('x')).toBe(14500); - expect(component.state('y')).toBe(4500); - - sinon.assert.notCalled(clearTooltipStateStub); - }); - - test('should clear tooltip state if tooltip location is outside map bounds', () => { - const component = mount( - - ); - - // move map bounds outside of hoverTooltipState.location, which is [-120, 30] - mockMbMapBounds = { - west: -180, - east: -170, - north: 90, - south: 80, - }; - mockMbMapHandlers.move(); - component.update(); - - sinon.assert.calledOnce(clearTooltipStateStub); - }); - }); - - test('should un-register all map callbacks on unmount', () => { - const component = mount(); - - expect(Object.keys(mockMbMapHandlers).length).toBe(4); - - component.unmount(); - expect(Object.keys(mockMbMapHandlers).length).toBe(0); - }); }); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js new file mode 100644 index 0000000000000..867c779bc4dba --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component } from 'react'; +import { LAT_INDEX, LON_INDEX } from '../../../../../common/constants'; +import { FeaturesTooltip } from '../../features_tooltip/features_tooltip'; +import { EuiPopover, EuiText } from '@elastic/eui'; + +const noop = () => {}; + +export class TooltipPopover extends Component { + state = { + x: undefined, + y: undefined, + isVisible: true, + }; + + constructor(props) { + super(props); + this._popoverRef = React.createRef(); + } + + componentDidMount() { + this._updatePopoverPosition(); + this.props.mbMap.on('move', this._updatePopoverPosition); + } + + componentDidUpdate() { + if (this._popoverRef.current) { + this._popoverRef.current.positionPopoverFluid(); + } + } + + componentWillUnmount() { + this.props.mbMap.off('move', this._updatePopoverPosition); + } + + _updatePopoverPosition = () => { + const nextPoint = this.props.mbMap.project(this.props.location); + const lat = this.props.location[LAT_INDEX]; + const lon = this.props.location[LON_INDEX]; + const bounds = this.props.mbMap.getBounds(); + this.setState({ + x: nextPoint.x, + y: nextPoint.y, + isVisible: + lat < bounds.getNorth() && + lat > bounds.getSouth() && + lon > bounds.getWest() && + lon < bounds.getEast(), + }); + }; + + // Must load original geometry instead of using geometry from mapbox feature. + // Mapbox feature geometry is from vector tile and is not the same as the original geometry. + _loadFeatureGeometry = ({ layerId, featureId }) => { + const tooltipLayer = this._findLayerById(layerId); + if (!tooltipLayer) { + return null; + } + + const targetFeature = tooltipLayer.getFeatureById(featureId); + if (!targetFeature) { + return null; + } + + return targetFeature.geometry; + }; + + _loadFeatureProperties = async ({ layerId, featureId }) => { + const tooltipLayer = this._findLayerById(layerId); + if (!tooltipLayer) { + return []; + } + + const targetFeature = tooltipLayer.getFeatureById(featureId); + if (!targetFeature) { + return []; + } + return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties); + }; + + _loadPreIndexedShape = async ({ layerId, featureId }) => { + const tooltipLayer = this._findLayerById(layerId); + if (!tooltipLayer) { + return null; + } + + const targetFeature = tooltipLayer.getFeatureById(featureId); + if (!targetFeature) { + return null; + } + + return await tooltipLayer.getSource().getPreIndexedShape(targetFeature.properties); + }; + + _findLayerById = layerId => { + return this.props.layerList.find(layer => { + return layer.getId() === layerId; + }); + }; + + _getLayerName = async layerId => { + const layer = this._findLayerById(layerId); + if (!layer) { + return null; + } + + return layer.getDisplayName(); + }; + + _renderTooltipContent = () => { + const publicProps = { + addFilters: this.props.addFilters, + closeTooltip: this.props.closeTooltip, + features: this.props.features, + isLocked: this.props.isLocked, + loadFeatureProperties: this._loadFeatureProperties, + loadFeatureGeometry: this._loadFeatureGeometry, + getLayerName: this._getLayerName, + }; + + if (this.props.renderTooltipContent) { + return this.props.renderTooltipContent(publicProps); + } + + return ( + + + + ); + }; + + render() { + if (!this.state.isVisible) { + return null; + } + + const tooltipAnchor =
; + // Although tooltip anchors are not visible, they take up horizontal space. + // This horizontal spacing needs to be accounted for in the translate function, + // otherwise the anchors get increasingly pushed to the right away from the actual location. + const offset = this.props.index * 26; + return ( + + {this._renderTooltipContent()} + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js new file mode 100644 index 0000000000000..bcef03c205b2b --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../features_tooltip/features_tooltip', () => ({ + FeaturesTooltip: () => { + return
mockFeaturesTooltip
; + }, +})); + +import sinon from 'sinon'; +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { TooltipPopover } from './tooltip_popover'; + +// mutable map state +let mapCenter; +let mockMbMapBounds; + +const layerId = 'tfi3f'; + +const mockMbMapHandlers = {}; +const mockMBMap = { + project: lonLatArray => { + const lonDistanceFromCenter = Math.abs(lonLatArray[0] - mapCenter[0]); + const latDistanceFromCenter = Math.abs(lonLatArray[1] - mapCenter[1]); + return { + x: lonDistanceFromCenter * 100, + y: latDistanceFromCenter * 100, + }; + }, + on: (eventName, callback) => { + mockMbMapHandlers[eventName] = callback; + }, + off: eventName => { + delete mockMbMapHandlers[eventName]; + }, + getBounds: () => { + return { + getNorth: () => { + return mockMbMapBounds.north; + }, + getSouth: () => { + return mockMbMapBounds.south; + }, + getWest: () => { + return mockMbMapBounds.west; + }, + getEast: () => { + return mockMbMapBounds.east; + }, + }; + }, +}; + +const defaultProps = { + mbMap: mockMBMap, + closeTooltip: () => {}, + layerList: [], + isDrawingFilter: false, + addFilters: () => {}, + geoFields: [{}], + location: [-120, 30], + features: [ + { + id: 1, + layerId: layerId, + geometry: {}, + }, + ], + isLocked: false, +}; + +describe('TooltipPopover', () => { + beforeEach(() => { + mapCenter = [0, 0]; + mockMbMapBounds = { + west: -180, + east: 180, + north: 90, + south: -90, + }; + }); + + describe('render', () => { + test('should render tooltip popover', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + test('should render tooltip popover with custom tooltip content when renderTooltipContent provided', () => { + const component = shallow( + { + return
Custom tooltip content
; + }} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + test('should un-register all map callbacks on unmount', () => { + const component = mount(); + + expect(Object.keys(mockMbMapHandlers).length).toBe(1); + + component.unmount(); + expect(Object.keys(mockMbMapHandlers).length).toBe(0); + }); + }); + + describe('on map move', () => { + const closeTooltipStub = sinon.stub(); + + beforeEach(() => { + closeTooltipStub.reset(); + }); + + test('should update popover location', () => { + const component = mount(); + + // ensure x and y set from original tooltipState.location + expect(component.state('x')).toBe(12000); + expect(component.state('y')).toBe(3000); + + mapCenter = [25, -15]; + mockMbMapHandlers.move(); + component.update(); + + // ensure x and y updated from new map center with same tooltipState.location + expect(component.state('x')).toBe(14500); + expect(component.state('y')).toBe(4500); + + sinon.assert.notCalled(closeTooltipStub); + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index b8644adddcf7e..93ef40162a584 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -29,6 +29,31 @@ import { loadIndexSettings } from './load_index_settings'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; import { ESDocField } from '../../fields/es_doc_field'; +function getField(indexPattern, fieldName) { + const field = indexPattern.fields.getByName(fieldName); + if (!field) { + throw new Error( + i18n.translate('xpack.maps.source.esSearch.fieldNotFoundMsg', { + defaultMessage: `Unable to find '{fieldName}' in index-pattern '{indexPatternTitle}'.`, + values: { fieldName, indexPatternTitle: indexPattern.title }, + }) + ); + } + return field; +} + +function addFieldToDSL(dsl, field) { + return !field.scripted + ? { ...dsl, field: field.name } + : { + ...dsl, + script: { + source: field.script, + lang: field.lang, + }, + }; +} + export class ESSearchSource extends AbstractESSource { static type = ES_SEARCH; static title = i18n.translate('xpack.maps.source.esSearchTitle', { @@ -242,7 +267,7 @@ export class ESSearchSource extends AbstractESSource { } async _getTopHits(layerName, searchFilters, registerCancelCallback) { - const { topHitsSplitField, topHitsSize } = this._descriptor; + const { topHitsSplitField: topHitsSplitFieldName, topHitsSize } = this._descriptor; const indexPattern = await this.getIndexPattern(); const geoField = await this._getGeoField(); @@ -279,20 +304,20 @@ export class ESSearchSource extends AbstractESSource { }; } + const topHitsSplitField = getField(indexPattern, topHitsSplitFieldName); + const cardinalityAgg = { precision_threshold: 1 }; + const termsAgg = { + size: DEFAULT_MAX_BUCKETS_LIMIT, + shard_size: DEFAULT_MAX_BUCKETS_LIMIT, + }; + const searchSource = await this._makeSearchSource(searchFilters, 0); searchSource.setField('aggs', { totalEntities: { - cardinality: { - field: topHitsSplitField, - precision_threshold: 1, - }, + cardinality: addFieldToDSL(cardinalityAgg, topHitsSplitField), }, entitySplit: { - terms: { - field: topHitsSplitField, - size: DEFAULT_MAX_BUCKETS_LIMIT, - shard_size: DEFAULT_MAX_BUCKETS_LIMIT, - }, + terms: addFieldToDSL(termsAgg, topHitsSplitField), aggs: { entityHits: { top_hits: topHits, diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap index 5837a80ec3083..f7dea92a8a0b7 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap @@ -38,8 +38,8 @@ exports[`Renders PolygonIcon 1`] = ` exports[`Renders SymbolIcon 1`] = ` `; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js index 301d64e676703..ea3886c600be9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js @@ -12,62 +12,30 @@ import { getMakiSymbolSvg, styleSvg, buildSrcUrl } from '../../symbol_utils'; export class SymbolIcon extends Component { state = { imgDataUrl: undefined, - prevSymbolId: undefined, - prevFill: undefined, - prevStroke: undefined, - prevStrokeWidth: undefined, }; componentDidMount() { this._isMounted = true; - this._loadSymbol( - this.props.symbolId, - this.props.fill, - this.props.stroke, - this.props.strokeWidth - ); - } - - componentDidUpdate() { - this._loadSymbol( - this.props.symbolId, - this.props.fill, - this.props.stroke, - this.props.strokeWidth - ); + this._loadSymbol(); } componentWillUnmount() { this._isMounted = false; } - async _loadSymbol(nextSymbolId, nextFill, nextStroke, nextStrokeWidth) { - if ( - nextSymbolId === this.state.prevSymbolId && - nextFill === this.state.prevFill && - nextStroke === this.state.prevStroke && - nextStrokeWidth === this.state.prevStrokeWidth - ) { - return; - } - + async _loadSymbol() { let imgDataUrl; try { - const svg = getMakiSymbolSvg(nextSymbolId); - const styledSvg = await styleSvg(svg, nextFill, nextStroke, nextStrokeWidth); + const svg = getMakiSymbolSvg(this.props.symbolId); + const styledSvg = await styleSvg(svg, this.props.fill, this.props.stroke); imgDataUrl = buildSrcUrl(styledSvg); } catch (error) { // ignore failures - component will just not display an icon + return; } if (this._isMounted) { - this.setState({ - imgDataUrl, - prevSymbolId: nextSymbolId, - prevFill: nextFill, - prevStroke: nextStroke, - prevStrokeWidth: nextStrokeWidth, - }); + this.setState({ imgDataUrl }); } } @@ -80,7 +48,6 @@ export class SymbolIcon extends Component { symbolId, // eslint-disable-line no-unused-vars fill, // eslint-disable-line no-unused-vars stroke, // eslint-disable-line no-unused-vars - strokeWidth, // eslint-disable-line no-unused-vars ...rest } = this.props; @@ -98,7 +65,6 @@ export class SymbolIcon extends Component { SymbolIcon.propTypes = { symbolId: PropTypes.string.isRequired, - fill: PropTypes.string.isRequired, - stroke: PropTypes.string.isRequired, - strokeWidth: PropTypes.string.isRequired, + fill: PropTypes.string, + stroke: PropTypes.string, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js index 29429b5b29aff..e255dceda856e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js @@ -37,10 +37,10 @@ export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, return ( ); } @@ -49,6 +49,6 @@ VectorIcon.propTypes = { fillColor: PropTypes.string, isPointsOnly: PropTypes.bool.isRequired, isLinesOnly: PropTypes.bool.isRequired, - strokeColor: PropTypes.string.isRequired, + strokeColor: PropTypes.string, symbolId: PropTypes.string, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap index b4b7a3fcf28fa..706dc0763b7ca 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap @@ -25,8 +25,6 @@ exports[`Should render icon select 1`] = ` } @@ -53,8 +51,6 @@ exports[`Should render icon select 1`] = ` "label": "symbol1", "prepend": , "value": "symbol1", @@ -63,8 +59,6 @@ exports[`Should render icon select 1`] = ` "label": "symbol2", "prepend": , "value": "symbol2", diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js index 03cd1ac14a013..68f7a30b22862 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js @@ -80,11 +80,10 @@ export class IconSelect extends Component { fullWidth prepend={ } /> @@ -100,10 +99,9 @@ export class IconSelect extends Component { label, prepend: ( ), }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 9636dab406a44..7daf85b68dd8e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -146,11 +146,6 @@ export class VectorStyleEditor extends Component { this.props.handlePropertyChange(propertyName, styleDescriptor); }; - _hasBorder() { - const width = this.props.styleProperties[VECTOR_STYLES.LINE_WIDTH]; - return width.isDynamic() ? width.isComplete() : width.getOptions().size !== 0; - } - _hasMarkerOrIcon() { const iconSize = this.props.styleProperties[VECTOR_STYLES.ICON_SIZE]; return iconSize.isDynamic() || iconSize.getOptions().size > 0; @@ -192,7 +187,7 @@ export class VectorStyleEditor extends Component { const disabledByIconSize = isPointBorderColor && !this._hasMarkerOrIcon(); return (
); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js index ed59b1d5513a0..1d3b3608cb2d9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js @@ -32,21 +32,12 @@ describe('styleSvg', () => { ); }); - it('Should add stroke style property to svg element', async () => { + it('Should add stroke and stroke-wdth style properties to svg element', async () => { const unstyledSvgString = ''; const styledSvg = await styleSvg(unstyledSvgString, 'red', 'white'); expect(styledSvg.split('\n')[1]).toBe( - '' - ); - }); - - it('Should add stroke-width style property to svg element', async () => { - const unstyledSvgString = - ''; - const styledSvg = await styleSvg(unstyledSvgString, 'red', 'white', '2px'); - expect(styledSvg.split('\n')[1]).toBe( - '' + '' ); }); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 1f96c37c9d286..62651fdd702d6 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -138,6 +138,12 @@ export class VectorStyle extends AbstractStyle { ]; } + _hasBorder() { + return this._lineWidthStyleProperty.isDynamic() + ? this._lineWidthStyleProperty.isComplete() + : this._lineWidthStyleProperty.getOptions().size !== 0; + } + renderEditor({ layer, onStyleDescriptorChange }) { const rawProperties = this.getRawProperties(); const handlePropertyChange = (propertyName, settings) => { @@ -170,6 +176,7 @@ export class VectorStyle extends AbstractStyle { onIsTimeAwareChange={onIsTimeAwareChange} isTimeAware={this.isTimeAware()} showIsTimeAware={propertiesWithFieldMeta.length > 0} + hasBorder={this._hasBorder()} /> ); } @@ -423,12 +430,18 @@ export class VectorStyle extends AbstractStyle { getIcon = () => { const isLinesOnly = this._getIsLinesOnly(); - const strokeColor = isLinesOnly - ? extractColorFromStyleProperty(this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], 'grey') - : extractColorFromStyleProperty( - this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], - 'none' - ); + let strokeColor; + if (isLinesOnly) { + strokeColor = extractColorFromStyleProperty( + this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], + 'grey' + ); + } else if (this._hasBorder()) { + strokeColor = extractColorFromStyleProperty( + this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], + 'none' + ); + } const fillColor = isLinesOnly ? null : extractColorFromStyleProperty( diff --git a/x-pack/legacy/plugins/maps/public/reducers/map.js b/x-pack/legacy/plugins/maps/public/reducers/map.js index 234584d08a311..7e81fb03dd85b 100644 --- a/x-pack/legacy/plugins/maps/public/reducers/map.js +++ b/x-pack/legacy/plugins/maps/public/reducers/map.js @@ -37,7 +37,7 @@ import { ROLLBACK_TO_TRACKED_LAYER_STATE, REMOVE_TRACKED_LAYER_STATE, UPDATE_SOURCE_DATA_REQUEST, - SET_TOOLTIP_STATE, + SET_OPEN_TOOLTIPS, SET_SCROLL_ZOOM, SET_MAP_INIT_ERROR, UPDATE_DRAW_STATE, @@ -97,7 +97,7 @@ const INITIAL_STATE = { ready: false, mapInitError: null, goto: null, - tooltipState: null, + openTooltips: [], mapState: { zoom: null, // setting this value does not adjust map zoom, read only value used to store current map zoom for persisting between sessions center: null, // setting this value does not adjust map view, read only value used to store current map center for persisting between sessions @@ -138,10 +138,10 @@ export function map(state = INITIAL_STATE, action) { return trackCurrentLayerState(state, action.layerId); case ROLLBACK_TO_TRACKED_LAYER_STATE: return rollbackTrackedLayerState(state, action.layerId); - case SET_TOOLTIP_STATE: + case SET_OPEN_TOOLTIPS: return { ...state, - tooltipState: action.tooltipState, + openTooltips: action.openTooltips, }; case SET_MOUSE_COORDINATES: return { diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js index 4b3d1355e4264..d1048a759beca 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js @@ -42,8 +42,14 @@ function createSourceInstance(sourceDescriptor, inspectorAdapters) { return new Source(sourceDescriptor, inspectorAdapters); } -export const getTooltipState = ({ map }) => { - return map.tooltipState; +export const getOpenTooltips = ({ map }) => { + return map && map.openTooltips ? map.openTooltips : []; +}; + +export const getHasLockedTooltips = state => { + return getOpenTooltips(state).some(({ isLocked }) => { + return isLocked; + }); }; export const getMapReady = ({ map }) => map && map.ready; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js index cc9593d946bd1..1ac391c7f84ae 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js @@ -21,7 +21,7 @@ export const jobs = { jobsWithTimerange(dateFormatTz) { return http({ - url: `${basePath()}/jobs/jobs_with_timerange`, + url: `${basePath()}/jobs/jobs_with_time_range`, method: 'POST', data: { dateFormatTz, diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts index 2487943b5efc0..61f21c316be23 100644 --- a/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts @@ -6,6 +6,7 @@ import { difference } from 'lodash'; import Boom from 'boom'; +import { IScopedClusterClient } from 'src/core/server'; import { EventManager, CalendarEvent } from './event_manager'; interface BasicCalendar { @@ -23,13 +24,12 @@ export interface FormCalendar extends BasicCalendar { } export class CalendarManager { - private _client: any; + private _client: IScopedClusterClient['callAsCurrentUser']; private _eventManager: any; - constructor(isLegacy: boolean, client: any) { - const actualClient = isLegacy === true ? client : client.ml!.mlClient.callAsCurrentUser; - this._client = actualClient; - this._eventManager = new EventManager(actualClient); + constructor(client: any) { + this._client = client; + this._eventManager = new EventManager(client); } async getCalendar(calendarId: string) { diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js index 58237b2a8a730..91f82f04a9a0c 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js @@ -7,7 +7,7 @@ import { CalendarManager } from '../calendar'; export function groupsProvider(callWithRequest) { - const calMngr = new CalendarManager(true, callWithRequest); + const calMngr = new CalendarManager(callWithRequest); async function getAllGroups() { const groups = {}; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/index.js b/x-pack/legacy/plugins/ml/server/models/job_service/index.js index 5c0eff3112a53..6f409e70e68b8 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/index.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/index.js @@ -14,14 +14,14 @@ import { topCategoriesProvider, } from './new_job'; -export function jobServiceProvider(callWithRequest, request) { +export function jobServiceProvider(callAsCurrentUser) { return { - ...datafeedsProvider(callWithRequest), - ...jobsProvider(callWithRequest), - ...groupsProvider(callWithRequest), - ...newJobCapsProvider(callWithRequest, request), - ...newJobChartsProvider(callWithRequest, request), - ...categorizationExamplesProvider(callWithRequest, request), - ...topCategoriesProvider(callWithRequest, request), + ...datafeedsProvider(callAsCurrentUser), + ...jobsProvider(callAsCurrentUser), + ...groupsProvider(callAsCurrentUser), + ...newJobCapsProvider(callAsCurrentUser), + ...newJobChartsProvider(callAsCurrentUser), + ...categorizationExamplesProvider(callAsCurrentUser), + ...topCategoriesProvider(callAsCurrentUser), }; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js b/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js index e60593c9f0ed5..b4b476c1f926e 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js @@ -22,7 +22,7 @@ export function jobsProvider(callWithRequest) { const { forceDeleteDatafeed, getDatafeedIdsByJobId } = datafeedsProvider(callWithRequest); const { getAuditMessagesSummary } = jobAuditMessagesProvider(callWithRequest); const { getLatestBucketTimestampByJob } = resultsServiceProvider(callWithRequest); - const calMngr = new CalendarManager(true, callWithRequest); + const calMngr = new CalendarManager(callWithRequest); async function forceDeleteJob(jobId) { return callWithRequest('ml.deleteJob', { jobId, force: true }); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index 3cfb552189062..5827201a63661 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -5,7 +5,7 @@ */ import { cloneDeep } from 'lodash'; -import { Request } from 'src/legacy/server/kbn_server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { Field, Aggregation, @@ -40,22 +40,27 @@ export function fieldServiceProvider( indexPattern: string, isRollup: boolean, callWithRequest: any, - request: Request + savedObjectsClient: SavedObjectsClientContract ) { - return new FieldsService(indexPattern, isRollup, callWithRequest, request); + return new FieldsService(indexPattern, isRollup, callWithRequest, savedObjectsClient); } class FieldsService { private _indexPattern: string; private _isRollup: boolean; private _callWithRequest: any; - private _request: Request; + private _savedObjectsClient: SavedObjectsClientContract; - constructor(indexPattern: string, isRollup: boolean, callWithRequest: any, request: Request) { + constructor( + indexPattern: string, + isRollup: boolean, + callWithRequest: any, + savedObjectsClient: any + ) { this._indexPattern = indexPattern; this._isRollup = isRollup; this._callWithRequest = callWithRequest; - this._request = request; + this._savedObjectsClient = savedObjectsClient; } private async loadFieldCaps(): Promise { @@ -104,7 +109,7 @@ class FieldsService { const rollupService = await rollupServiceProvider( this._indexPattern, this._callWithRequest, - this._request + this._savedObjectsClient ); const rollupConfigs: RollupJob[] | null = await rollupService.getRollupJobs(); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts index 2c8f8a8f82fb8..f1af7614b4232 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts @@ -18,7 +18,7 @@ import cloudwatchJobCaps from './__mocks__/results/cloudwatch_rollup_job_caps.js describe('job_service - job_caps', () => { let callWithRequestNonRollupMock: jest.Mock; let callWithRequestRollupMock: jest.Mock; - let requestMock: any; + let savedObjectsClientMock: any; beforeEach(() => { callWithRequestNonRollupMock = jest.fn((action: string) => { @@ -37,14 +37,10 @@ describe('job_service - job_caps', () => { } }); - requestMock = { - getSavedObjectsClient: jest.fn(() => { - return { - async find() { - return Promise.resolve(kibanaSavedObjects); - }, - }; - }), + savedObjectsClientMock = { + async find() { + return Promise.resolve(kibanaSavedObjects); + }, }; }); @@ -52,8 +48,8 @@ describe('job_service - job_caps', () => { it('can get job caps for index pattern', async done => { const indexPattern = 'farequote-*'; const isRollup = false; - const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(farequoteJobCaps); done(); }); @@ -61,8 +57,8 @@ describe('job_service - job_caps', () => { it('can get rollup job caps for non rollup index pattern', async done => { const indexPattern = 'farequote-*'; const isRollup = true; - const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(farequoteJobCapsEmpty); done(); }); @@ -72,8 +68,8 @@ describe('job_service - job_caps', () => { it('can get rollup job caps for rollup index pattern', async done => { const indexPattern = 'cloud_roll_index'; const isRollup = true; - const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(cloudwatchJobCaps); done(); }); @@ -81,8 +77,8 @@ describe('job_service - job_caps', () => { it('can get non rollup job caps for rollup index pattern', async done => { const indexPattern = 'cloud_roll_index'; const isRollup = false; - const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).not.toEqual(cloudwatchJobCaps); done(); }); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts index cbb249be09aa0..3a9d979ccb22c 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Request } from 'src/legacy/server/kbn_server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { Aggregation, Field, NewJobCaps } from '../../../../common/types/fields'; import { fieldServiceProvider } from './field_service'; @@ -12,12 +12,18 @@ interface NewJobCapsResponse { [indexPattern: string]: NewJobCaps; } -export function newJobCapsProvider(callWithRequest: any, request: Request) { +export function newJobCapsProvider(callWithRequest: any) { async function newJobCaps( indexPattern: string, - isRollup: boolean = false + isRollup: boolean = false, + savedObjectsClient: SavedObjectsClientContract ): Promise { - const fieldService = fieldServiceProvider(indexPattern, isRollup, callWithRequest, request); + const fieldService = fieldServiceProvider( + indexPattern, + isRollup, + callWithRequest, + savedObjectsClient + ); const { aggs, fields } = await fieldService.getData(); convertForStringify(aggs, fields); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts index 5f8d8ae5c1f25..11b0802192e1f 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Request } from 'src/legacy/server/kbn_server'; import { SavedObject } from 'src/core/server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { FieldId } from '../../../../common/types/fields'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; @@ -21,9 +21,9 @@ export interface RollupJob { export async function rollupServiceProvider( indexPattern: string, callWithRequest: any, - request: Request + savedObjectsClient: SavedObjectsClientContract ) { - const rollupIndexPatternObject = await loadRollupIndexPattern(indexPattern, request); + const rollupIndexPatternObject = await loadRollupIndexPattern(indexPattern, savedObjectsClient); let jobIndexPatterns: string[] = [indexPattern]; async function getRollupJobs(): Promise { @@ -57,9 +57,8 @@ export async function rollupServiceProvider( async function loadRollupIndexPattern( indexPattern: string, - request: Request + savedObjectsClient: SavedObjectsClientContract ): Promise { - const savedObjectsClient = request.getSavedObjectsClient(); const resp = await savedObjectsClient.find({ type: 'index-pattern', fields: ['title', 'type', 'typeMeta'], diff --git a/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts index 392d3bfd84768..d728fbf312d76 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts @@ -6,6 +6,18 @@ import { schema } from '@kbn/config-schema'; +const customRulesSchema = schema.maybe( + schema.arrayOf( + schema.maybe( + schema.object({ + actions: schema.arrayOf(schema.string()), + conditions: schema.arrayOf(schema.any()), + scope: schema.maybe(schema.any()), + }) + ) + ) +); + const detectorSchema = schema.object({ identifier: schema.maybe(schema.string()), function: schema.string(), @@ -14,6 +26,7 @@ const detectorSchema = schema.object({ over_field_name: schema.maybe(schema.string()), partition_field_name: schema.maybe(schema.string()), detector_description: schema.maybe(schema.string()), + custom_rules: customRulesSchema, }); const customUrlSchema = { @@ -34,15 +47,8 @@ export const anomalyDetectionUpdateJobSchema = { schema.maybe( schema.object({ detector_index: schema.number(), - custom_rules: schema.arrayOf( - schema.maybe( - schema.object({ - actions: schema.arrayOf(schema.string()), - conditions: schema.arrayOf(schema.any()), - scope: schema.maybe(schema.any()), - }) - ) - ), + description: schema.maybe(schema.string()), + custom_rules: customRulesSchema, }) ) ) diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts new file mode 100644 index 0000000000000..b37fcba737802 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +const analyzerSchema = { + tokenizer: schema.string(), + filter: schema.maybe( + schema.arrayOf( + schema.object({ + type: schema.string(), + stopwords: schema.arrayOf(schema.maybe(schema.string())), + }) + ) + ), +}; + +export const categorizationFieldExamplesSchema = { + indexPatternTitle: schema.string(), + query: schema.any(), + size: schema.number(), + field: schema.string(), + timeField: schema.maybe(schema.string()), + start: schema.number(), + end: schema.number(), + analyzer: schema.object(analyzerSchema), +}; + +export const chartSchema = { + indexPatternTitle: schema.string(), + timeField: schema.maybe(schema.string()), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), + intervalMs: schema.number(), + query: schema.any(), + aggFieldNamePairs: schema.arrayOf(schema.any()), + splitFieldName: schema.maybe(schema.nullable(schema.string())), + splitFieldValue: schema.maybe(schema.nullable(schema.string())), +}; + +export const datafeedIdsSchema = { datafeedIds: schema.arrayOf(schema.maybe(schema.string())) }; + +export const forceStartDatafeedSchema = { + datafeedIds: schema.arrayOf(schema.maybe(schema.string())), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), +}; + +export const jobIdsSchema = { + jobIds: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.maybe(schema.string()))]) + ), +}; + +export const jobsWithTimerangeSchema = { dateFormatTz: schema.maybe(schema.string()) }; + +export const lookBackProgressSchema = { + jobId: schema.string(), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), +}; + +export const topCategoriesSchema = { jobId: schema.string(), count: schema.number() }; + +export const updateGroupsSchema = { + jobs: schema.maybe( + schema.arrayOf( + schema.object({ + job_id: schema.maybe(schema.string()), + groups: schema.arrayOf(schema.maybe(schema.string())), + }) + ) + ), +}; diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json index 919592f8ed62a..3fac715fef85a 100644 --- a/x-pack/legacy/plugins/ml/server/routes/apidoc.json +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -50,6 +50,25 @@ "Annotations", "GetAnnotations", "IndexAnnotations", - "DeleteAnnotation" + "DeleteAnnotation", + "JobService", + "ForceStartDatafeeds", + "StopDatafeeds", + "DeleteJobs", + "CloseJobs", + "JobsSummary", + "JobsWithTimerange", + "CreateFullJobsList", + "GetAllGroups", + "UpdateGroups", + "DeletingJobTasks", + "JobsExist", + "NewJobCaps", + "NewJobLineChart", + "NewJobPopulationChart", + "GetAllJobAndGroupIds", + "GetLookBackProgress", + "ValidateCategoryExamples", + "TopCategories" ] } diff --git a/x-pack/legacy/plugins/ml/server/routes/calendars.ts b/x-pack/legacy/plugins/ml/server/routes/calendars.ts index 19d614a4e6a22..8e4e1c4c14751 100644 --- a/x-pack/legacy/plugins/ml/server/routes/calendars.ts +++ b/x-pack/legacy/plugins/ml/server/routes/calendars.ts @@ -13,32 +13,32 @@ import { calendarSchema } from '../new_platform/calendars_schema'; import { CalendarManager, Calendar, FormCalendar } from '../models/calendar'; function getAllCalendars(context: RequestHandlerContext) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getAllCalendars(); } function getCalendar(context: RequestHandlerContext, calendarId: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getCalendar(calendarId); } function newCalendar(context: RequestHandlerContext, calendar: FormCalendar) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.newCalendar(calendar); } function updateCalendar(context: RequestHandlerContext, calendarId: string, calendar: Calendar) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.updateCalendar(calendarId, calendar); } function deleteCalendar(context: RequestHandlerContext, calendarId: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.deleteCalendar(calendarId); } function getCalendarsByIds(context: RequestHandlerContext, calendarIds: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getCalendarsByIds(calendarIds); } diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.js b/x-pack/legacy/plugins/ml/server/routes/job_service.js deleted file mode 100644 index a83b4fa403f65..0000000000000 --- a/x-pack/legacy/plugins/ml/server/routes/job_service.js +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../client/call_with_request_factory'; -import { wrapError } from '../client/errors'; -import { jobServiceProvider } from '../models/job_service'; - -export function jobServiceRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - route({ - method: 'POST', - path: '/api/ml/jobs/force_start_datafeeds', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { forceStartDatafeeds } = jobServiceProvider(callWithRequest); - const { datafeedIds, start, end } = request.payload; - return forceStartDatafeeds(datafeedIds, start, end).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/stop_datafeeds', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { stopDatafeeds } = jobServiceProvider(callWithRequest); - const { datafeedIds } = request.payload; - return stopDatafeeds(datafeedIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/delete_jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { deleteJobs } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return deleteJobs(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/close_jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { closeJobs } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return closeJobs(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_summary', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsSummary } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return jobsSummary(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_with_timerange', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsWithTimerange } = jobServiceProvider(callWithRequest); - const { dateFormatTz } = request.payload; - return jobsWithTimerange(dateFormatTz).catch(resp => { - wrapError(resp); - }); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { createFullJobsList } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return createFullJobsList(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/groups', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getAllGroups } = jobServiceProvider(callWithRequest); - return getAllGroups().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/update_groups', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { updateGroups } = jobServiceProvider(callWithRequest); - const { jobs } = request.payload; - return updateGroups(jobs).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/deleting_jobs_tasks', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { deletingJobTasks } = jobServiceProvider(callWithRequest); - return deletingJobTasks().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_exist', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsExist } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return jobsExist(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/new_job_caps/{indexPattern}', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { indexPattern } = request.params; - const isRollup = request.query.rollup === 'true'; - const { newJobCaps } = jobServiceProvider(callWithRequest, request); - return newJobCaps(indexPattern, isRollup).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/new_job_line_chart', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - splitFieldValue, - } = request.payload; - const { newJobLineChart } = jobServiceProvider(callWithRequest, request); - return newJobLineChart( - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - splitFieldValue - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/new_job_population_chart', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - } = request.payload; - const { newJobPopulationChart } = jobServiceProvider(callWithRequest, request); - return newJobPopulationChart( - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/all_jobs_and_group_ids', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getAllJobAndGroupIds } = jobServiceProvider(callWithRequest); - return getAllJobAndGroupIds().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/look_back_progress', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getLookBackProgress } = jobServiceProvider(callWithRequest); - const { jobId, start, end } = request.payload; - return getLookBackProgress(jobId, start, end).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/categorization_field_examples', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { validateCategoryExamples } = jobServiceProvider(callWithRequest); - const { - indexPatternTitle, - timeField, - query, - size, - field, - start, - end, - analyzer, - } = request.payload; - return validateCategoryExamples( - indexPatternTitle, - query, - size, - field, - timeField, - start, - end, - analyzer - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/top_categories', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { topCategories } = jobServiceProvider(callWithRequest); - const { jobId, count } = request.payload; - return topCategories(jobId, count).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); -} diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.ts b/x-pack/legacy/plugins/ml/server/routes/job_service.ts new file mode 100644 index 0000000000000..3af651c92353b --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/job_service.ts @@ -0,0 +1,610 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { wrapError } from '../client/error_wrapper'; +import { RouteInitialization } from '../new_platform/plugin'; +import { + categorizationFieldExamplesSchema, + chartSchema, + datafeedIdsSchema, + forceStartDatafeedSchema, + jobIdsSchema, + jobsWithTimerangeSchema, + lookBackProgressSchema, + topCategoriesSchema, + updateGroupsSchema, +} from '../new_platform/job_service_schema'; +// @ts-ignore no declaration module +import { jobServiceProvider } from '../models/job_service'; + +/** + * Routes for job service + */ +export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitialization) { + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/force_start_datafeeds + * @apiName ForceStartDatafeeds + * @apiDescription Starts one or more datafeeds + */ + router.post( + { + path: '/api/ml/jobs/force_start_datafeeds', + validate: { + body: schema.object(forceStartDatafeedSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { forceStartDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { datafeedIds, start, end } = request.body; + const resp = await forceStartDatafeeds(datafeedIds, start, end); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/stop_datafeeds + * @apiName StopDatafeeds + * @apiDescription Stops one or more datafeeds + */ + router.post( + { + path: '/api/ml/jobs/stop_datafeeds', + validate: { + body: schema.object(datafeedIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { stopDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { datafeedIds } = request.body; + const resp = await stopDatafeeds(datafeedIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/delete_jobs + * @apiName DeleteJobs + * @apiDescription Deletes an existing anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/delete_jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { deleteJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await deleteJobs(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/close_jobs + * @apiName CloseJobs + * @apiDescription Closes one or more anomaly detection jobs + */ + router.post( + { + path: '/api/ml/jobs/close_jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { closeJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await closeJobs(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_summary + * @apiName JobsSummary + * @apiDescription Creates a summary jobs list. Jobs include job stats, datafeed stats, and calendars. + */ + router.post( + { + path: '/api/ml/jobs/jobs_summary', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsSummary } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await jobsSummary(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_with_time_range + * @apiName JobsWithTimerange + * @apiDescription Creates a list of jobs with data about the job's timerange + */ + router.post( + { + path: '/api/ml/jobs/jobs_with_time_range', + validate: { + body: schema.object(jobsWithTimerangeSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsWithTimerange } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { dateFormatTz } = request.body; + const resp = await jobsWithTimerange(dateFormatTz); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs + * @apiName CreateFullJobsList + * @apiDescription Creates a list of jobs + */ + router.post( + { + path: '/api/ml/jobs/jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { createFullJobsList } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await createFullJobsList(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/groups + * @apiName GetAllGroups + * @apiDescription Returns array of group objects with job ids listed for each group + */ + router.get( + { + path: '/api/ml/jobs/groups', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getAllGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await getAllGroups(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/update_groups + * @apiName UpdateGroups + * @apiDescription Updates 'groups' property of an anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/update_groups', + validate: { + body: schema.object(updateGroupsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { updateGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobs } = request.body; + const resp = await updateGroups(jobs); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/deleting_jobs_tasks + * @apiName DeletingJobTasks + * @apiDescription Gets the ids of deleting anomaly detection jobs + */ + router.get( + { + path: '/api/ml/jobs/deleting_jobs_tasks', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { deletingJobTasks } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await deletingJobTasks(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_exist + * @apiName JobsExist + * @apiDescription Checks if each of the jobs in the specified list of IDs exist + */ + router.post( + { + path: '/api/ml/jobs/jobs_exist', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsExist } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await jobsExist(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/new_job_caps/:indexPattern + * @apiName NewJobCaps + * @apiDescription Retrieve the capabilities of fields for indices + */ + router.get( + { + path: '/api/ml/jobs/new_job_caps/{indexPattern}', + validate: { + params: schema.object({ indexPattern: schema.string() }), + query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { indexPattern } = request.params; + const isRollup = request.query.rollup === 'true'; + const savedObjectsClient = context.core.savedObjects.client; + const { newJobCaps } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await newJobCaps(indexPattern, isRollup, savedObjectsClient); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/new_job_line_chart + * @apiName NewJobLineChart + * @apiDescription Returns line chart data for anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/new_job_line_chart', + validate: { + body: schema.object(chartSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue, + } = request.body; + + const { newJobLineChart } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser, + request + ); + const resp = await newJobLineChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/new_job_population_chart + * @apiName NewJobPopulationChart + * @apiDescription Returns population job chart data + */ + router.post( + { + path: '/api/ml/jobs/new_job_population_chart', + validate: { + body: schema.object(chartSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + } = request.body; + + const { newJobPopulationChart } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser + ); + const resp = await newJobPopulationChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/all_jobs_and_group_ids + * @apiName GetAllJobAndGroupIds + * @apiDescription Returns a list of all job IDs and all group IDs + */ + router.get( + { + path: '/api/ml/jobs/all_jobs_and_group_ids', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getAllJobAndGroupIds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await getAllJobAndGroupIds(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/look_back_progress + * @apiName GetLookBackProgress + * @apiDescription Returns current progress of anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/look_back_progress', + validate: { + body: schema.object(lookBackProgressSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getLookBackProgress } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobId, start, end } = request.body; + const resp = await getLookBackProgress(jobId, start, end); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/categorization_field_examples + * @apiName ValidateCategoryExamples + * @apiDescription Validates category examples + */ + router.post( + { + path: '/api/ml/jobs/categorization_field_examples', + validate: { + body: schema.object(categorizationFieldExamplesSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { validateCategoryExamples } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser + ); + const { + indexPatternTitle, + timeField, + query, + size, + field, + start, + end, + analyzer, + } = request.body; + + const resp = await validateCategoryExamples( + indexPatternTitle, + query, + size, + field, + timeField, + start, + end, + analyzer + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/top_categories + * @apiName TopCategories + * @apiDescription Returns list of top categories + */ + router.post( + { + path: '/api/ml/jobs/top_categories', + validate: { + body: schema.object(topCategoriesSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { topCategories } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobId, count } = request.body; + const resp = await topCategories(jobId, count); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index fd89c40f010b7..18b815fb429cb 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -115,9 +115,6 @@ export const security = kibana => const xpackInfo = server.plugins.xpack_main.info; securityPlugin.__legacyCompat.registerLegacyAPI({ auditLogger: new AuditLogger(server, 'security', config, xpackInfo), - isSystemAPIRequest: server.plugins.kibana.systemApi.isSystemApiRequest.bind( - server.plugins.kibana.systemApi - ), }); // Legacy xPack Info endpoint returns whatever we return in a callback for `registerLicenseCheckResultsGenerator` diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts index 24c1974cf8343..3d251c1c6bcac 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts @@ -4,30 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HOSTS_PAGE } from '../../../urls/navigation'; import { + waitForAllHostsToBeLoaded, + dragAndDropFirstHostToTimeline, + dragFirstHostToTimeline, + dragFirstHostToEmptyTimelineDataProviders, +} from '../../../tasks/hosts/all_hosts'; +import { HOSTS_NAMES } from '../../../screens/hosts/all_hosts'; +import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../../tasks/login'; +import { openTimeline, createNewTimeline } from '../../../tasks/timeline/main'; +import { + TIMELINE_DATA_PROVIDERS_EMPTY, TIMELINE_DATA_PROVIDERS, TIMELINE_DROPPED_DATA_PROVIDERS, - TIMELINE_DATA_PROVIDERS_EMPTY, -} from '../../lib/timeline/selectors'; -import { - createNewTimeline, - dragFromAllHostsToTimeline, - toggleTimelineVisibility, -} from '../../lib/timeline/helpers'; -import { ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS } from '../../lib/hosts/selectors'; -import { HOSTS_PAGE } from '../../lib/urls'; -import { waitForAllHostsWidget } from '../../lib/hosts/helpers'; -import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../lib/util/helpers'; -import { drag, dragWithoutDrop } from '../../lib/drag_n_drop/helpers'; +} from '../../../screens/timeline/main'; describe('timeline data providers', () => { before(() => { loginAndWaitForPage(HOSTS_PAGE); - waitForAllHostsWidget(); + waitForAllHostsToBeLoaded(); }); beforeEach(() => { - toggleTimelineVisibility(); + openTimeline(); }); afterEach(() => { @@ -35,16 +35,13 @@ describe('timeline data providers', () => { }); it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { - dragFromAllHostsToTimeline(); + dragAndDropFirstHostToTimeline(); - cy.get(TIMELINE_DROPPED_DATA_PROVIDERS, { - timeout: DEFAULT_TIMEOUT + 10 * 1000, - }) + cy.get(TIMELINE_DROPPED_DATA_PROVIDERS, { timeout: DEFAULT_TIMEOUT }) .first() .invoke('text') .then(dataProviderText => { - // verify the data provider displays the same `host.name` as the host dragged from the `All Hosts` widget - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) + cy.get(HOSTS_NAMES) .first() .invoke('text') .should(hostname => { @@ -54,9 +51,7 @@ describe('timeline data providers', () => { }); it('sets the background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the data providers', () => { - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) - .first() - .then(host => drag(host)); + dragFirstHostToTimeline(); cy.get(TIMELINE_DATA_PROVIDERS).should( 'have.css', @@ -65,30 +60,14 @@ describe('timeline data providers', () => { ); }); - it('sets the background to euiColorSuccess with a 20% alpha channel when the user starts dragging a host AND is hovering over the data providers', () => { - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) - .first() - .then(host => drag(host)); - - cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).then(dataProvidersDropArea => - dragWithoutDrop(dataProvidersDropArea) - ); + it('sets the background to euiColorSuccess with a 20% alpha channel and renders the dashed border color as euiColorSuccess when the user starts dragging a host AND is hovering over the data providers', () => { + dragFirstHostToEmptyTimelineDataProviders(); cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).should( 'have.css', 'background', 'rgba(1, 125, 115, 0.2) none repeat scroll 0% 0% / auto padding-box border-box' ); - }); - - it('renders the dashed border color as euiColorSuccess when hovering over the data providers', () => { - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) - .first() - .then(host => drag(host)); - - cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).then(dataProvidersDropArea => - dragWithoutDrop(dataProvidersDropArea) - ); cy.get(TIMELINE_DATA_PROVIDERS).should( 'have.css', diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts b/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts new file mode 100644 index 0000000000000..f316356580814 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ALL_HOSTS_TABLE = '[data-test-subj="table-allHosts-loading-false"]'; + +export const HOSTS_NAMES = '[data-test-subj="draggable-content-host.name"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts index ca11f48932263..60c9c2ab44372 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts @@ -20,3 +20,15 @@ export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]'; + +export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; + +export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; + +export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; + +export const TIMELINE_DATA_PROVIDERS_EMPTY = + '[data-test-subj="dataProviders"] [data-test-subj="empty"]'; + +export const TIMELINE_DROPPED_DATA_PROVIDERS = + '[data-test-subj="dataProviders"] [data-test-subj="providerContainer"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/common.ts b/x-pack/legacy/plugins/siem/cypress/tasks/common.ts new file mode 100644 index 0000000000000..39a61401c15b3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/common.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const primaryButton = 0; + +/** + * To overcome the React Beautiful DND sloppy click detection threshold: + * https://github.com/atlassian/react-beautiful-dnd/blob/67b96c8d04f64af6b63ae1315f74fc02b5db032b/docs/sensors/mouse.md#sloppy-clicks-and-click-prevention- + */ +const dndSloppyClickDetectionThreshold = 5; + +/** Starts dragging the subject */ +export const drag = (subject: JQuery) => { + const subjectLocation = subject[0].getBoundingClientRect(); + + cy.wrap(subject) + .trigger('mousedown', { + button: primaryButton, + clientX: subjectLocation.left, + clientY: subjectLocation.top, + force: true, + }) + .wait(1) + .trigger('mousemove', { + button: primaryButton, + clientX: subjectLocation.left + dndSloppyClickDetectionThreshold, + clientY: subjectLocation.top, + force: true, + }) + .wait(1); +}; + +/** "Drops" the subject being dragged on the specified drop target */ +export const drop = (dropTarget: JQuery) => { + cy.wrap(dropTarget) + .trigger('mousemove', { button: primaryButton, force: true }) + .wait(1) + .trigger('mouseup', { force: true }) + .wait(1); +}; + +/** Drags the subject being dragged on the specified drop target, but does not drop it */ +export const dragWithoutDrop = (dropTarget: JQuery) => { + cy.wrap(dropTarget).trigger('mousemove', 'center', { + button: primaryButton, + }); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts new file mode 100644 index 0000000000000..43e2a7e1bef11 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ALL_HOSTS_TABLE, HOSTS_NAMES } from '../../screens/hosts/all_hosts'; +import { + TIMELINE_DATA_PROVIDERS, + TIMELINE_DATA_PROVIDERS_EMPTY, +} from '../../screens/timeline/main'; +import { DEFAULT_TIMEOUT } from '../../tasks/login'; +import { drag, drop, dragWithoutDrop } from '../../tasks/common'; + +export const waitForAllHostsToBeLoaded = () => { + cy.get(ALL_HOSTS_TABLE, { timeout: DEFAULT_TIMEOUT }).should('exist'); +}; + +export const dragAndDropFirstHostToTimeline = () => { + cy.get(HOSTS_NAMES) + .first() + .then(firstHost => drag(firstHost)); + cy.get(TIMELINE_DATA_PROVIDERS).then(dataProvidersDropArea => drop(dataProvidersDropArea)); +}; + +export const dragFirstHostToTimeline = () => { + cy.get(HOSTS_NAMES) + .first() + .then(host => drag(host)); +}; + +export const dragFirstHostToEmptyTimelineDataProviders = () => { + cy.get(HOSTS_NAMES) + .first() + .then(host => drag(host)); + + cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).then(dataProvidersDropArea => + dragWithoutDrop(dataProvidersDropArea) + ); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts index ae2a863092907..068b6dd9f8bd4 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts @@ -13,6 +13,8 @@ import { SERVER_SIDE_EVENT_COUNT, TIMELINE_SETTINGS_ICON, TIMELINE_INSPECT_BUTTON, + CREATE_NEW_TIMELINE, + CLOSE_TIMELINE_BTN, } from '../../screens/timeline/main'; export const hostExistsQuery = 'host.name: *'; @@ -44,3 +46,9 @@ export const openTimelineInspectButton = () => { cy.get(TIMELINE_INSPECT_BUTTON, { timeout: DEFAULT_TIMEOUT }).should('not.be.disabled'); cy.get(TIMELINE_INSPECT_BUTTON).trigger('click', { force: true }); }; + +export const createNewTimeline = () => { + cy.get(TIMELINE_SETTINGS_ICON).click({ force: true }); + cy.get(CREATE_NEW_TIMELINE).click(); + cy.get(CLOSE_TIMELINE_BTN).click({ force: true }); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts index 0437693e87e5e..164a117b82475 100644 --- a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts +++ b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts @@ -6,6 +6,7 @@ export const TIMELINES_PAGE = '/app/siem#/timelines'; export const OVERVIEW_PAGE = '/app/siem#/overview'; +export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; export const HOSTS_PAGE_TAB_URLS = { allHosts: '/app/siem#/hosts/allHosts', anomalies: '/app/siem#/hosts/anomalies', diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_page_new/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..1b792503cf1c6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/__snapshots__/index.test.tsx.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HeaderPage it renders 1`] = ` +
+ + + +

+ Test title + + +

+ + + + + +

+ Test supplement +

+
+ + +`; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx new file mode 100644 index 0000000000000..83a70fd90d82b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../mock'; +import { HeaderPage } from './index'; +import { useMountAppended } from '../../utils/use_mount_appended'; + +describe('HeaderPage', () => { + const mount = useMountAppended(); + + test('it renders', () => { + const wrapper = shallow( + +

{'Test supplement'}

+
+ ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the title', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-title"]') + .first() + .exists() + ).toBe(true); + }); + + test('it renders the back link when provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('.siemHeaderPage__linkBack') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the back link when not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('.siemHeaderPage__linkBack') + .first() + .exists() + ).toBe(false); + }); + + test('it renders the first subtitle when provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-subtitle"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the first subtitle when not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-section-subtitle"]') + .first() + .exists() + ).toBe(false); + }); + + test('it renders the second subtitle when provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-subtitle-2"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the second subtitle when not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-section-subtitle-2"]') + .first() + .exists() + ).toBe(false); + }); + + test('it renders supplements when children provided', () => { + const wrapper = mount( + + +

{'Test supplement'}

+
+
+ ); + + expect( + wrapper + .find('[data-test-subj="header-page-supplements"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render supplements when children not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-supplements"]') + .first() + .exists() + ).toBe(false); + }); + + test('it applies border styles when border is true', () => { + const wrapper = mount( + + + + ); + const siemHeaderPage = wrapper.find('.siemHeaderPage').first(); + + expect(siemHeaderPage).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); + expect(siemHeaderPage).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); + }); + + test('it DOES NOT apply border styles when border is false', () => { + const wrapper = mount( + + + + ); + const siemHeaderPage = wrapper.find('.siemHeaderPage').first(); + + expect(siemHeaderPage).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); + expect(siemHeaderPage).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); + }); + + test('it renders as a draggable when arguments provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-draggable"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render as a draggable when arguments not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-draggable"]') + .first() + .exists() + ).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx new file mode 100644 index 0000000000000..7e486c78fb9b9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx @@ -0,0 +1,220 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBadge, + EuiBetaBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiTitle, +} from '@elastic/eui'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +import { DefaultDraggable } from '../draggables'; +import { LinkIcon, LinkIconProps } from '../link_icon'; +import { Subtitle, SubtitleProps } from '../subtitle'; +import * as i18n from './translations'; + +interface HeaderProps { + border?: boolean; + isLoading?: boolean; +} + +const Header = styled.header.attrs({ + className: 'siemHeaderPage', +})` + ${({ border, theme }) => css` + margin-bottom: ${theme.eui.euiSizeL}; + + ${border && + css` + border-bottom: ${theme.eui.euiBorderThin}; + padding-bottom: ${theme.eui.paddingSizes.l}; + .euiProgress { + top: ${theme.eui.paddingSizes.l}; + } + `} + `} +`; +Header.displayName = 'Header'; + +const FlexItem = styled(EuiFlexItem)` + display: block; +`; +FlexItem.displayName = 'FlexItem'; + +const LinkBack = styled.div.attrs({ + className: 'siemHeaderPage__linkBack', +})` + ${({ theme }) => css` + font-size: ${theme.eui.euiFontSizeXS}; + line-height: ${theme.eui.euiLineHeight}; + margin-bottom: ${theme.eui.euiSizeS}; + `} +`; +LinkBack.displayName = 'LinkBack'; + +const Badge = styled(EuiBadge)` + letter-spacing: 0; +`; +Badge.displayName = 'Badge'; + +const StyledEuiBetaBadge = styled(EuiBetaBadge)` + vertical-align: middle; +`; + +StyledEuiBetaBadge.displayName = 'StyledEuiBetaBadge'; + +const StyledEuiButtonIcon = styled(EuiButtonIcon)` + ${({ theme }) => css` + margin-left: ${theme.eui.euiSize}; + `} +`; + +StyledEuiButtonIcon.displayName = 'StyledEuiButtonIcon'; + +interface BackOptions { + href: LinkIconProps['href']; + text: LinkIconProps['children']; +} + +interface BadgeOptions { + beta?: boolean; + text: string; + tooltip?: string; +} + +interface DraggableArguments { + field: string; + value: string; +} +interface IconAction { + 'aria-label': string; + iconType: string; + onChange: (a: string) => void; + onClick: (b: boolean) => void; + onSubmit: () => void; +} + +export interface HeaderPageProps extends HeaderProps { + backOptions?: BackOptions; + badgeOptions?: BadgeOptions; + children?: React.ReactNode; + draggableArguments?: DraggableArguments; + isEditTitle?: boolean; + iconAction?: IconAction; + subtitle2?: SubtitleProps['items']; + subtitle?: SubtitleProps['items']; + title: string | React.ReactNode; +} + +const HeaderPageComponent: React.FC = ({ + backOptions, + badgeOptions, + border, + children, + draggableArguments, + isEditTitle, + iconAction, + isLoading, + subtitle, + subtitle2, + title, + ...rest +}) => ( +
+ + + {backOptions && ( + + + {backOptions.text} + + + )} + + {isEditTitle && iconAction ? ( + + + iconAction.onChange(e.target.value)} + value={`${title}`} + /> + + + + + {i18n.SUBMIT} + + + + iconAction.onClick(false)}> + {i18n.CANCEL} + + + + + + ) : ( + +

+ {!draggableArguments ? ( + title + ) : ( + + )} + {badgeOptions && ( + <> + {' '} + {badgeOptions.beta ? ( + + ) : ( + {badgeOptions.text} + )} + + )} + {iconAction && ( + iconAction.onClick(true)} + /> + )} +

+
+ )} + + {subtitle && } + {subtitle2 && } + {border && isLoading && } +
+ + {children && {children}} +
+
+); + +export const HeaderPage = React.memo(HeaderPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/translations.ts b/x-pack/legacy/plugins/siem/public/components/header_page_new/translations.ts new file mode 100644 index 0000000000000..57b2cda0b0b01 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SUBMIT = i18n.translate('xpack.siem.case.casePage.title.submit', { + defaultMessage: 'Submit', +}); + +export const CANCEL = i18n.translate('xpack.siem.case.casePage.title.cancel', { + defaultMessage: 'Cancel', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts index ad6147e5aad76..c93b415e017bb 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts @@ -13,3 +13,10 @@ export { getOverviewUrl, RedirectToOverviewPage } from './redirect_to_overview'; export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts'; export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network'; export { getTimelinesUrl, RedirectToTimelinesPage } from './redirect_to_timelines'; +export { + getCaseDetailsUrl, + getCaseUrl, + getCreateCaseUrl, + RedirectToCasePage, + RedirectToCreatePage, +} from './redirect_to_case'; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index dc8c696301611..c08b429dc4625 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -20,6 +20,7 @@ import { RedirectToHostsPage, RedirectToHostDetailsPage } from './redirect_to_ho import { RedirectToNetworkPage } from './redirect_to_network'; import { RedirectToOverviewPage } from './redirect_to_overview'; import { RedirectToTimelinesPage } from './redirect_to_timelines'; +import { RedirectToCasePage, RedirectToCreatePage } from './redirect_to_case'; import { DetectionEngineTab } from '../../pages/detection_engine/types'; interface LinkToPageProps { @@ -32,6 +33,20 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToOverviewPage} path={`${match.url}/:pageName(${SiemPageName.overview})`} /> + + + ; + +export const RedirectToCasePage = ({ + match: { + params: { detailName }, + }, +}: CaseComponentProps) => ( + +); + +export const RedirectToCreatePage = () => ; + +const baseCaseUrl = `#/link-to/${SiemPageName.case}`; + +export const getCaseUrl = () => baseCaseUrl; +export const getCaseDetailsUrl = (detailName: string) => `${baseCaseUrl}/${detailName}`; +export const getCreateCaseUrl = () => `${baseCaseUrl}/create`; diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index e122b3e235a9e..4f74f9ff2f5d6 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -8,7 +8,12 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; import { encodeIpv6 } from '../../lib/helpers'; -import { getHostDetailsUrl, getIPDetailsUrl } from '../link_to'; +import { + getCaseDetailsUrl, + getHostDetailsUrl, + getIPDetailsUrl, + getCreateCaseUrl, +} from '../link_to'; import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; // Internal Links @@ -35,6 +40,23 @@ const IPDetailsLinkComponent: React.FC<{ export const IPDetailsLink = React.memo(IPDetailsLinkComponent); +const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailName: string }> = ({ + children, + detailName, +}) => ( + + {children ? children : detailName} + +); +export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); +CaseDetailsLink.displayName = 'CaseDetailsLink'; + +export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => ( + {children} +)); + +CreateCaseLink.displayName = 'CreateCaseLink'; + // External Links export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts index e8d5032fd7548..e25fb4374bb14 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -11,6 +11,7 @@ import { APP_NAME } from '../../../../common/constants'; import { StartServices } from '../../../plugin'; import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; +import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../pages/case/utils'; import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../pages/detection_engine/rules/utils'; import { SiemPageName } from '../../../pages/home/types'; import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types'; @@ -43,6 +44,9 @@ const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpySt const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => spyState != null && spyState.pageName === SiemPageName.hosts; +const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => + spyState != null && spyState.pageName === SiemPageName.case; + const isDetectionsRoutes = (spyState: RouteSpyState) => spyState != null && spyState.pageName === SiemPageName.detections; @@ -102,6 +106,9 @@ export const getBreadcrumbsForRoute = ( ), ]; } + if (isCaseRoutes(spyState) && object.navTabs) { + return [...siemRootBreadcrumb, ...getCaseDetailsBreadcrumbs(spyState)]; + } if ( spyState != null && object.navTabs && diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index ac7a4a0ee52b7..8eb08bd3d62f0 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -66,6 +66,13 @@ describe('SIEM Navigation', () => { { detailName: undefined, navTabs: { + case: { + disabled: true, + href: '#/link-to/case', + id: 'case', + name: 'Case', + urlKey: 'case', + }, detections: { disabled: false, href: '#/link-to/detections', @@ -152,6 +159,13 @@ describe('SIEM Navigation', () => { detailName: undefined, filters: [], navTabs: { + case: { + disabled: true, + href: '#/link-to/case', + id: 'case', + name: 'Case', + urlKey: 'case', + }, detections: { disabled: false, href: '#/link-to/detections', diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx index 5a3439d53dd89..15e58f3efd21e 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx @@ -32,8 +32,6 @@ const TextArea = styled(EuiTextArea)<{ height: number }>` TextArea.displayName = 'TextArea'; -TextArea.displayName = 'TextArea'; - /** An input for entering a new note */ export const NewNote = React.memo<{ noteInputHeight: number; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts index 22e8f99658f8d..b6ef3c8ccd4e9 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts @@ -6,6 +6,8 @@ export enum CONSTANTS { appQuery = 'query', + caseDetails = 'case.details', + casePage = 'case.page', detectionsPage = 'detections.page', filters = 'filters', hostsDetails = 'hosts.details', @@ -14,10 +16,10 @@ export enum CONSTANTS { networkPage = 'network.page', overviewPage = 'overview.page', savedQuery = 'savedQuery', + timeline = 'timeline', timelinePage = 'timeline.page', timerange = 'timerange', - timeline = 'timeline', unknown = 'unknown', } -export type UrlStateType = 'detections' | 'host' | 'network' | 'overview' | 'timeline'; +export type UrlStateType = 'case' | 'detections' | 'host' | 'network' | 'overview' | 'timeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index 7be775ef0c0e4..05329621aa97a 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -98,6 +98,8 @@ export const getUrlType = (pageName: string): UrlStateType => { return 'detections'; } else if (pageName === SiemPageName.timelines) { return 'timeline'; + } else if (pageName === SiemPageName.case) { + return 'case'; } return 'overview'; }; @@ -131,6 +133,11 @@ export const getCurrentLocation = ( return CONSTANTS.detectionsPage; } else if (pageName === SiemPageName.timelines) { return CONSTANTS.timelinePage; + } else if (pageName === SiemPageName.case) { + if (detailName != null) { + return CONSTANTS.caseDetails; + } + return CONSTANTS.casePage; } return CONSTANTS.unknown; }; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index fea1bc016fd49..97979e514aeaf 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -60,9 +60,12 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.timeline, ], timeline: [CONSTANTS.timeline, CONSTANTS.timerange], + case: [], }; export type LocationTypes = + | CONSTANTS.caseDetails + | CONSTANTS.casePage | CONSTANTS.detectionsPage | CONSTANTS.hostsDetails | CONSTANTS.hostsPage diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts new file mode 100644 index 0000000000000..830e00c70975e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaServices } from '../../lib/kibana'; +import { AllCases, FetchCasesProps, Case, NewCase, SortFieldCase } from './types'; +import { Direction } from '../../graphql/types'; +import { throwIfNotOk } from '../../hooks/api/api'; +import { CASES_URL } from './constants'; + +export const getCase = async (caseId: string, includeComments: boolean) => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { + method: 'GET', + asResponse: true, + query: { + includeComments, + }, + }); + await throwIfNotOk(response.response); + return response.body!; +}; + +export const getCases = async ({ + filterOptions = { + search: '', + tags: [], + }, + queryParams = { + page: 1, + perPage: 20, + sortField: SortFieldCase.createdAt, + sortOrder: Direction.desc, + }, +}: FetchCasesProps): Promise => { + const tags = [...(filterOptions.tags?.map(t => `case-workflow.attributes.tags: ${t}`) ?? [])]; + const query = { + ...queryParams, + filter: tags.join(' AND '), + search: filterOptions.search, + }; + const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + method: 'GET', + query, + asResponse: true, + }); + await throwIfNotOk(response.response); + return response.body!; +}; + +export const createCase = async (newCase: NewCase): Promise => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + method: 'POST', + asResponse: true, + body: JSON.stringify(newCase), + }); + await throwIfNotOk(response.response); + return response.body!; +}; + +export const updateCaseProperty = async ( + caseId: string, + updatedCase: Partial +): Promise> => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { + method: 'PATCH', + asResponse: true, + body: JSON.stringify(updatedCase), + }); + await throwIfNotOk(response.response); + return response.body!; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts new file mode 100644 index 0000000000000..c8d668527ae32 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const CASES_URL = `/api/cases`; +export const DEFAULT_TABLE_ACTIVE_PAGE = 1; +export const DEFAULT_TABLE_LIMIT = 5; +export const FETCH_FAILURE = 'FETCH_FAILURE'; +export const FETCH_INIT = 'FETCH_INIT'; +export const FETCH_SUCCESS = 'FETCH_SUCCESS'; +export const POST_NEW_CASE = 'POST_NEW_CASE'; +export const UPDATE_CASE_PROPERTY = 'UPDATE_CASE_PROPERTY'; +export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; +export const UPDATE_QUERY_PARAMS = 'UPDATE_QUERY_PARAMS'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts new file mode 100644 index 0000000000000..0c8b896e2b426 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_TITLE = i18n.translate('xpack.siem.containers.case.errorTitle', { + defaultMessage: 'Error fetching data', +}); + +export const TAG_FETCH_FAILURE = i18n.translate( + 'xpack.siem.containers.case.tagFetchFailDescription', + { + defaultMessage: 'Failed to fetch Tags', + } +); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts new file mode 100644 index 0000000000000..0f80b2327a30c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Direction } from '../../graphql/types'; +interface FormData { + isNew?: boolean; +} + +export interface NewCase extends FormData { + description: string; + tags: string[]; + title: string; +} + +export interface Case { + case_id: string; + created_at: string; + created_by: ElasticUser; + description: string; + state: string; + tags: string[]; + title: string; + updated_at: string; +} + +export interface QueryParams { + page: number; + perPage: number; + sortField: SortFieldCase; + sortOrder: Direction; +} + +export interface FilterOptions { + search: string; + tags: string[]; +} + +export interface AllCases { + cases: Case[]; + page: number; + per_page: number; + total: number; +} +export enum SortFieldCase { + createdAt = 'created_at', + state = 'state', + updatedAt = 'updated_at', +} + +export interface ElasticUser { + readonly username: string; + readonly full_name?: string; +} + +export interface FetchCasesProps { + queryParams?: QueryParams; + filterOptions?: FilterOptions; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx new file mode 100644 index 0000000000000..8cc961c68fdf0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useReducer } from 'react'; + +import { Case } from './types'; +import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; +import { getTypedPayload } from './utils'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { useStateToaster } from '../../components/toasters'; +import { getCase } from './api'; + +interface CaseState { + data: Case; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: Case; +} + +const dataFetchReducer = (state: CaseState, action: Action): CaseState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: Case = { + case_id: '', + created_at: '', + created_by: { + username: '', + }, + description: '', + state: '', + tags: [], + title: '', + updated_at: '', +}; + +export const useGetCase = (caseId: string): [CaseState] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: true, + isError: false, + data: initialData, + }); + const [, dispatchToaster] = useStateToaster(); + + const callFetch = () => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const response = await getCase(caseId, false); + if (!didCancel) { + dispatch({ type: FETCH_SUCCESS, payload: response }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }; + + useEffect(() => { + callFetch(); + }, [caseId]); + return [state]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx new file mode 100644 index 0000000000000..db9c07747ba04 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { isEqual } from 'lodash/fp'; +import { + DEFAULT_TABLE_ACTIVE_PAGE, + DEFAULT_TABLE_LIMIT, + FETCH_FAILURE, + FETCH_INIT, + FETCH_SUCCESS, + UPDATE_QUERY_PARAMS, + UPDATE_FILTER_OPTIONS, +} from './constants'; +import { AllCases, SortFieldCase, FilterOptions, QueryParams } from './types'; +import { getTypedPayload } from './utils'; +import { Direction } from '../../graphql/types'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { useStateToaster } from '../../components/toasters'; +import * as i18n from './translations'; +import { getCases } from './api'; + +export interface UseGetCasesState { + data: AllCases; + isLoading: boolean; + isError: boolean; + queryParams: QueryParams; + filterOptions: FilterOptions; +} + +export interface QueryArgs { + page?: number; + perPage?: number; + sortField?: SortFieldCase; + sortOrder?: Direction; +} + +export interface Action { + type: string; + payload?: AllCases | QueryArgs | FilterOptions; +} +const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + case UPDATE_QUERY_PARAMS: + return { + ...state, + queryParams: { + ...state.queryParams, + ...action.payload, + }, + }; + case UPDATE_FILTER_OPTIONS: + return { + ...state, + filterOptions: getTypedPayload(action.payload), + }; + default: + throw new Error(); + } +}; + +const initialData: AllCases = { + page: 0, + per_page: 0, + total: 0, + cases: [], +}; +export const useGetCases = (): [ + UseGetCasesState, + Dispatch>, + Dispatch> +] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + filterOptions: { + search: '', + tags: [], + }, + queryParams: { + page: DEFAULT_TABLE_ACTIVE_PAGE, + perPage: DEFAULT_TABLE_LIMIT, + sortField: SortFieldCase.createdAt, + sortOrder: Direction.desc, + }, + }); + const [queryParams, setQueryParams] = useState(state.queryParams as QueryArgs); + const [filterQuery, setFilters] = useState(state.filterOptions as FilterOptions); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + if (!isEqual(queryParams, state.queryParams)) { + dispatch({ type: UPDATE_QUERY_PARAMS, payload: queryParams }); + } + }, [queryParams, state.queryParams]); + + useEffect(() => { + if (!isEqual(filterQuery, state.filterOptions)) { + dispatch({ type: UPDATE_FILTER_OPTIONS, payload: filterQuery }); + } + }, [filterQuery, state.filterOptions]); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const response = await getCases({ + filterOptions: state.filterOptions, + queryParams: state.queryParams, + }); + if (!didCancel) { + dispatch({ + type: FETCH_SUCCESS, + payload: response, + }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, [state.queryParams, state.filterOptions]); + return [state, setQueryParams, setFilters]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx new file mode 100644 index 0000000000000..f796ae550c9ec --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useReducer } from 'react'; +import chrome from 'ui/chrome'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; +import { throwIfNotOk } from '../../hooks/api/api'; + +interface TagsState { + data: string[]; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: string[]; +} + +const dataFetchReducer = (state: TagsState, action: Action): TagsState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + const getTypedPayload = (a: Action['payload']) => a as string[]; + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: string[] = []; + +export const useGetTags = (): [TagsState] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const response = await fetch(`${chrome.getBasePath()}/api/cases/tags`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-system-api': 'true', + }, + }); + if (!didCancel) { + await throwIfNotOk(response); + const responseJson = await response.json(); + dispatch({ type: FETCH_SUCCESS, payload: responseJson }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, []); + return [state]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx new file mode 100644 index 0000000000000..5cf99701977d2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, POST_NEW_CASE } from './constants'; +import { Case, NewCase } from './types'; +import { createCase } from './api'; +import { getTypedPayload } from './utils'; + +interface NewCaseState { + data: NewCase; + newCase?: Case; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: NewCase | Case; +} + +const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case POST_NEW_CASE: + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + newCase: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: NewCase = { + description: '', + isNew: false, + tags: [], + title: '', +}; + +export const usePostCase = (): [NewCaseState, Dispatch>] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [formData, setFormData] = useState(initialData); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + dispatch({ type: POST_NEW_CASE, payload: formData }); + }, [formData]); + + useEffect(() => { + const postCase = async () => { + dispatch({ type: FETCH_INIT }); + try { + const dataWithoutIsNew = state.data; + delete dataWithoutIsNew.isNew; + const response = await createCase(dataWithoutIsNew); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + }; + if (state.data.isNew) { + postCase(); + } + }, [state.data.isNew]); + return [state, setFormData]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx new file mode 100644 index 0000000000000..68592c17e58dc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useReducer } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, UPDATE_CASE_PROPERTY } from './constants'; +import { Case } from './types'; +import { updateCaseProperty } from './api'; +import { getTypedPayload } from './utils'; + +type UpdateKey = keyof Case; + +interface NewCaseState { + data: Case; + isLoading: boolean; + isError: boolean; + updateKey?: UpdateKey | null; +} + +interface UpdateByKey { + updateKey: UpdateKey; + updateValue: Case[UpdateKey]; +} + +interface Action { + type: string; + payload?: Partial | UpdateByKey; +} + +const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + updateKey: null, + }; + case UPDATE_CASE_PROPERTY: + const { updateKey, updateValue } = getTypedPayload(action.payload); + return { + ...state, + isLoading: false, + isError: false, + data: { + ...state.data, + [updateKey]: updateValue, + }, + updateKey, + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + data: { + ...state.data, + ...getTypedPayload(action.payload), + }, + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; + +export const useUpdateCase = ( + caseId: string, + initialData: Case +): [{ data: Case }, (updates: UpdateByKey) => void] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [, dispatchToaster] = useStateToaster(); + + const dispatchUpdateCaseProperty = ({ updateKey, updateValue }: UpdateByKey) => { + dispatch({ + type: UPDATE_CASE_PROPERTY, + payload: { updateKey, updateValue }, + }); + }; + + useEffect(() => { + const updateData = async (updateKey: keyof Case) => { + dispatch({ type: FETCH_INIT }); + try { + const response = await updateCaseProperty(caseId, { [updateKey]: state.data[updateKey] }); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + }; + if (state.updateKey) { + updateData(state.updateKey); + } + }, [state.updateKey]); + + return [{ data: state.data }, dispatchUpdateCaseProperty]; +}; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/index.ts b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts similarity index 79% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/index.ts rename to x-pack/legacy/plugins/siem/public/containers/case/utils.ts index f75dce9b7507f..8e6eaca1a8f0c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/index.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export const getTypedPayload = (a: unknown): T => a as T; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx new file mode 100644 index 0000000000000..1206ec950deed --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiButton, EuiFlexGroup } from '@elastic/eui'; +import { HeaderPage } from '../../components/header_page'; +import { WrapperPage } from '../../components/wrapper_page'; +import { AllCases } from './components/all_cases'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import * as i18n from './translations'; +import { getCreateCaseUrl } from '../../components/link_to'; + +const badgeOptions = { + beta: true, + text: i18n.PAGE_BADGE_LABEL, + tooltip: i18n.PAGE_BADGE_TOOLTIP, +}; + +export const CasesPage = React.memo(() => ( + <> + + + + + {i18n.CREATE_TITLE} + + + + + + + +)); + +CasesPage.displayName = 'CasesPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx new file mode 100644 index 0000000000000..890df91c8560e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { CaseView } from './components/case_view'; +import { SpyRoute } from '../../utils/route/spy_routes'; + +export const CaseDetailsPage = React.memo(() => { + const { detailName: caseId } = useParams(); + if (!caseId) { + return null; + } + return ( + <> + + + + ); +}); + +CaseDetailsPage.displayName = 'CaseDetailsPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx new file mode 100644 index 0000000000000..92cd16fd2000e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiBadge, EuiTableFieldDataColumnType, EuiTableComputedColumnType } from '@elastic/eui'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { Case } from '../../../../containers/case/types'; +import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; +import { CaseDetailsLink } from '../../../../components/links'; +import { TruncatableText } from '../../../../components/truncatable_text'; +import * as i18n from './translations'; + +export type CasesColumns = EuiTableFieldDataColumnType | EuiTableComputedColumnType; + +const renderStringField = (field: string) => (field != null ? field : getEmptyTagValue()); + +export const getCasesColumns = (): CasesColumns[] => [ + { + name: i18n.CASE_TITLE, + render: (theCase: Case) => { + if (theCase.case_id != null && theCase.title != null) { + return {theCase.title}; + } + return getEmptyTagValue(); + }, + }, + { + field: 'tags', + name: i18n.TAGS, + render: (tags: Case['tags']) => { + if (tags != null && tags.length > 0) { + return ( + + {tags.map((tag: string, i: number) => ( + + {tag} + + ))} + + ); + } + return getEmptyTagValue(); + }, + truncateText: true, + }, + { + field: 'created_at', + name: i18n.CREATED_AT, + sortable: true, + render: (createdAt: Case['created_at']) => { + if (createdAt != null) { + return ; + } + return getEmptyTagValue(); + }, + }, + { + field: 'created_by.username', + name: i18n.REPORTER, + render: (createdBy: Case['created_by']['username']) => renderStringField(createdBy), + }, + { + field: 'updated_at', + name: i18n.LAST_UPDATED, + sortable: true, + render: (updatedAt: Case['updated_at']) => { + if (updatedAt != null) { + return ; + } + return getEmptyTagValue(); + }, + }, + { + field: 'state', + name: i18n.STATE, + sortable: true, + render: (state: Case['state']) => renderStringField(state), + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx new file mode 100644 index 0000000000000..b1dd39c95e191 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { + EuiBasicTable, + EuiButton, + EuiEmptyPrompt, + EuiLoadingContent, + EuiTableSortingType, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import * as i18n from './translations'; + +import { getCasesColumns } from './columns'; +import { SortFieldCase, Case, FilterOptions } from '../../../../containers/case/types'; + +import { Direction } from '../../../../graphql/types'; +import { useGetCases } from '../../../../containers/case/use_get_cases'; +import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; +import { Panel } from '../../../../components/panel'; +import { HeaderSection } from '../../../../components/header_section'; +import { CasesTableFilters } from './table_filters'; + +import { + UtilityBar, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/detection_engine/utility_bar'; +import { getCreateCaseUrl } from '../../../../components/link_to'; + +export const AllCases = React.memo(() => { + const [ + { data, isLoading, queryParams, filterOptions }, + setQueryParams, + setFilters, + ] = useGetCases(); + + const tableOnChangeCallback = useCallback( + ({ page, sort }: EuiBasicTableOnChange) => { + let newQueryParams = queryParams; + if (sort) { + let newSort; + switch (sort.field) { + case 'state': + newSort = SortFieldCase.state; + break; + case 'created_at': + newSort = SortFieldCase.createdAt; + break; + case 'updated_at': + newSort = SortFieldCase.updatedAt; + break; + default: + newSort = SortFieldCase.createdAt; + } + newQueryParams = { + ...newQueryParams, + sortField: newSort, + sortOrder: sort.direction as Direction, + }; + } + if (page) { + newQueryParams = { + ...newQueryParams, + page: page.index + 1, + perPage: page.size, + }; + } + setQueryParams(newQueryParams); + }, + [setQueryParams, queryParams] + ); + + const onFilterChangedCallback = useCallback( + (newFilterOptions: Partial) => { + setFilters({ ...filterOptions, ...newFilterOptions }); + }, + [filterOptions, setFilters] + ); + + const memoizedGetCasesColumns = useMemo(() => getCasesColumns(), []); + const memoizedPagination = useMemo( + () => ({ + pageIndex: queryParams.page - 1, + pageSize: queryParams.perPage, + totalItemCount: data.total, + pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], + }), + [data, queryParams] + ); + + const sorting: EuiTableSortingType = { + sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, + }; + + return ( + + + + + {isLoading && isEmpty(data.cases) && ( + + )} + {!isLoading && !isEmpty(data.cases) && ( + <> + + + + {i18n.SHOWING_CASES(data.total ?? 0)} + + + + {i18n.NO_CASES}} + titleSize="xs" + body={i18n.NO_CASES_BODY} + actions={ + + {i18n.ADD_NEW_CASE} + + } + /> + } + onChange={tableOnChangeCallback} + pagination={memoizedPagination} + sorting={sorting} + /> + + )} + + ); +}); + +AllCases.displayName = 'AllCases'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx new file mode 100644 index 0000000000000..e593623788046 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useState } from 'react'; +import { isEqual } from 'lodash/fp'; +import { EuiFieldSearch, EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import * as i18n from './translations'; + +import { FilterOptions } from '../../../../containers/case/types'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; +import { TagsFilterPopover } from '../../../../pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover'; + +interface Initial { + search: string; + tags: string[]; +} +interface CasesTableFiltersProps { + onFilterChanged: (filterOptions: Partial) => void; + initial: Initial; +} + +/** + * Collection of filters for filtering data within the CasesTable. Contains search bar, + * and tag selection + * + * @param onFilterChanged change listener to be notified on filter changes + */ + +const CasesTableFiltersComponent = ({ + onFilterChanged, + initial = { search: '', tags: [] }, +}: CasesTableFiltersProps) => { + const [search, setSearch] = useState(initial.search); + const [selectedTags, setSelectedTags] = useState(initial.tags); + const [{ isLoading, data }] = useGetTags(); + + const handleSelectedTags = useCallback( + newTags => { + if (!isEqual(newTags, selectedTags)) { + setSelectedTags(newTags); + onFilterChanged({ search, tags: newTags }); + } + }, + [search, selectedTags] + ); + const handleOnSearch = useCallback( + newSearch => { + const trimSearch = newSearch.trim(); + if (!isEqual(trimSearch, search)) { + setSearch(trimSearch); + onFilterChanged({ tags: selectedTags, search: trimSearch }); + } + }, + [search, selectedTags] + ); + + return ( + + + + + + + + + + + + ); +}; + +CasesTableFiltersComponent.displayName = 'CasesTableFiltersComponent'; + +export const CasesTableFilters = React.memo(CasesTableFiltersComponent); + +CasesTableFilters.displayName = 'CasesTableFilters'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts new file mode 100644 index 0000000000000..ab8e22ebcf1be --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../translations'; + +export const ALL_CASES = i18n.translate('xpack.siem.case.caseTable.title', { + defaultMessage: 'All Cases', +}); +export const NO_CASES = i18n.translate('xpack.siem.case.caseTable.noCases.title', { + defaultMessage: 'No Cases', +}); +export const NO_CASES_BODY = i18n.translate('xpack.siem.case.caseTable.noCases.body', { + defaultMessage: 'Create a new case to see it displayed in the case workflow table.', +}); +export const ADD_NEW_CASE = i18n.translate('xpack.siem.case.caseTable.addNewCase', { + defaultMessage: 'Add New Case', +}); + +export const SHOWING_CASES = (totalRules: number) => + i18n.translate('xpack.siem.case.caseTable.showingCasesTitle', { + values: { totalRules }, + defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {case} other {cases}}', + }); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.siem.case.caseTable.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {case} other {cases}}`, + }); + +export const SEARCH_CASES = i18n.translate( + 'xpack.siem.detectionEngine.case.caseTable.searchAriaLabel', + { + defaultMessage: 'Search cases', + } +); + +export const SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.siem.detectionEngine.case.caseTable.searchPlaceholder', + { + defaultMessage: 'e.g. case name', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx new file mode 100644 index 0000000000000..4f43a6edeeac6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -0,0 +1,312 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonToggle, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, +} from '@elastic/eui'; + +import styled, { css } from 'styled-components'; +import * as i18n from './translations'; +import { DescriptionMarkdown } from '../description_md_editor'; +import { Case } from '../../../../containers/case/types'; +import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; +import { getCaseUrl } from '../../../../components/link_to'; +import { HeaderPage } from '../../../../components/header_page_new'; +import { Markdown } from '../../../../components/markdown'; +import { PropertyActions } from '../property_actions'; +import { TagList } from '../tag_list'; +import { useGetCase } from '../../../../containers/case/use_get_case'; +import { UserActionTree } from '../user_action_tree'; +import { UserList } from '../user_list'; +import { useUpdateCase } from '../../../../containers/case/use_update_case'; +import { WrapperPage } from '../../../../components/wrapper_page'; + +interface Props { + caseId: string; +} + +const MyDescriptionList = styled(EuiDescriptionList)` + ${({ theme }) => css` + & { + padding-right: ${theme.eui.euiSizeL}; + border-right: ${theme.eui.euiBorderThin}; + } + `} +`; + +const MyWrapper = styled(WrapperPage)` + padding-bottom: 0; +`; +const BackgroundWrapper = styled.div` + ${({ theme }) => css` + background-color: ${theme.eui.euiColorEmptyShade}; + border-top: ${theme.eui.euiBorderThin}; + height: 100%; + `} +`; + +interface CasesProps { + caseId: string; + initialData: Case; + isLoading: boolean; +} + +export const Cases = React.memo(({ caseId, initialData, isLoading }) => { + const [{ data }, dispatchUpdateCaseProperty] = useUpdateCase(caseId, initialData); + const [isEditDescription, setIsEditDescription] = useState(false); + const [isEditTitle, setIsEditTitle] = useState(false); + const [isEditTags, setIsEditTags] = useState(false); + const [isCaseOpen, setIsCaseOpen] = useState(data.state === 'open'); + const [description, setDescription] = useState(data.description); + const [title, setTitle] = useState(data.title); + const [tags, setTags] = useState(data.tags); + + const onUpdateField = useCallback( + async (updateKey: keyof Case, updateValue: string | string[]) => { + switch (updateKey) { + case 'title': + if (updateValue.length > 0) { + dispatchUpdateCaseProperty({ + updateKey: 'title', + updateValue, + }); + setIsEditTitle(false); + } + break; + case 'description': + if (updateValue.length > 0) { + dispatchUpdateCaseProperty({ + updateKey: 'description', + updateValue, + }); + setIsEditDescription(false); + } + break; + case 'tags': + setTags(updateValue as string[]); + if (updateValue.length > 0) { + dispatchUpdateCaseProperty({ + updateKey: 'tags', + updateValue, + }); + setIsEditTags(false); + } + break; + default: + return null; + } + }, + [dispatchUpdateCaseProperty, title] + ); + + const onSetIsCaseOpen = useCallback(() => setIsCaseOpen(!isCaseOpen), [ + isCaseOpen, + setIsCaseOpen, + ]); + + useEffect(() => { + const caseState = isCaseOpen ? 'open' : 'closed'; + if (data.state !== caseState) { + dispatchUpdateCaseProperty({ + updateKey: 'state', + updateValue: caseState, + }); + } + }, [isCaseOpen]); + + // TO DO refactor each of these const's into their own components + const propertyActions = [ + { + iconType: 'documentEdit', + label: 'Edit description', + onClick: () => setIsEditDescription(true), + }, + { + iconType: 'securitySignalResolved', + label: 'Close case', + onClick: () => null, + }, + { + iconType: 'trash', + label: 'Delete case', + onClick: () => null, + }, + { + iconType: 'importAction', + label: 'Push as ServiceNow incident', + onClick: () => null, + }, + { + iconType: 'popout', + label: 'View ServiceNow incident', + onClick: () => null, + }, + { + iconType: 'importAction', + label: 'Update ServiceNow incident', + onClick: () => null, + }, + ]; + const userActions = [ + { + avatarName: data.created_by.username, + title: ( + + +

+ {`${data.created_by.username}`} + {` ${i18n.ADDED_DESCRIPTION} `}{' '} + + {/* STEPH FIX come back and add label `on` */} +

+
+ + + +
+ ), + children: isEditDescription ? ( + <> + setDescription(updatedDescription)} + /> + + + + onUpdateField('description', description)} + > + {i18n.SUBMIT} + + + + setIsEditDescription(false)}> + {i18n.CANCEL} + + + + + ) : ( + + ), + }, + ]; + return ( + <> + + setTitle(newTitle), + onSubmit: () => onUpdateField('title', title), + onClick: isEdit => setIsEditTitle(isEdit), + }} + isEditTitle={isEditTitle} + title={title} + > + + + + + + {i18n.STATUS} + + {data.state} + + + + {i18n.CASE_OPENED} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + onUpdateField('tags', newTags), + onClick: isEdit => setIsEditTags(isEdit), + }} + isEditTags={isEditTags} + /> + + + + + + ); +}); + +export const CaseView = React.memo(({ caseId }: Props) => { + const [{ data, isLoading, isError }] = useGetCase(caseId); + if (isError) { + return null; + } + if (isLoading) { + return ( + + + + + + ); + } + + return ; +}); + +CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts new file mode 100644 index 0000000000000..f45c52533d2e7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../translations'; + +export const SHOWING_CASES = (actionDate: string, actionName: string, userName: string) => + i18n.translate('xpack.siem.case.caseView.actionHeadline', { + values: { + actionDate, + actionName, + userName, + }, + defaultMessage: '{userName} {actionName} on {actionDate}', + }); + +export const ADDED_DESCRIPTION = i18n.translate( + 'xpack.siem.case.caseView.actionLabel.addDescription', + { + defaultMessage: 'added description', + } +); + +export const EDITED_DESCRIPTION = i18n.translate( + 'xpack.siem.case.caseView.actionLabel.editDescription', + { + defaultMessage: 'edited description', + } +); + +export const ADDED_COMMENT = i18n.translate('xpack.siem.case.caseView.actionLabel.addComment', { + defaultMessage: 'added comment', +}); + +export const STATUS = i18n.translate('xpack.siem.case.caseView.statusLabel', { + defaultMessage: 'Status', +}); + +export const CASE_OPENED = i18n.translate('xpack.siem.case.caseView.caseOpened', { + defaultMessage: 'Case opened', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts new file mode 100644 index 0000000000000..7bc43e23a72c5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const stateOptions = [ + { + value: 'open', + inputDisplay: 'Open', + }, + { + value: 'closed', + inputDisplay: 'Closed', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx new file mode 100644 index 0000000000000..9fd1525003b0b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback } from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLoadingSpinner, + EuiPanel, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { Redirect } from 'react-router-dom'; +import { Field, Form, getUseField, useForm } from '../../../shared_imports'; +import { NewCase } from '../../../../containers/case/types'; +import { usePostCase } from '../../../../containers/case/use_post_case'; +import { schema } from './schema'; +import * as i18n from '../../translations'; +import { SiemPageName } from '../../../home/types'; +import { DescriptionMarkdown } from '../description_md_editor'; + +export const CommonUseField = getUseField({ component: Field }); + +const TagContainer = styled.div` + margin-top: 16px; +`; +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; +`; + +export const Create = React.memo(() => { + const [{ data, isLoading, newCase }, setFormData] = usePostCase(); + const { form } = useForm({ + defaultValue: data, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid) { + setFormData({ ...newData, isNew: true } as NewCase); + } + }, [form]); + + if (newCase && newCase.case_id) { + return ; + } + return ( + + {isLoading && } +
+ + setFormData({ ...data, description })} + /> + + + + + <> + + + + + {i18n.SUBMIT} + + + + +
+ ); +}); + +Create.displayName = 'Create'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx new file mode 100644 index 0000000000000..b86198e09ceac --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiText } from '@elastic/eui'; +import React from 'react'; + +import * as i18n from '../../../translations'; + +export const OptionalFieldLabel = ( + + {i18n.OPTIONAL} + +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx new file mode 100644 index 0000000000000..1b5df72a6671c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; +import { OptionalFieldLabel } from './optional_field_label'; +import * as i18n from '../../translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + title: { + type: FIELD_TYPES.TEXT, + label: i18n.CASE_TITLE, + validations: [ + { + validator: emptyField(i18n.TITLE_REQUIRED), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + validations: [ + { + validator: emptyField(i18n.DESCRIPTION_REQUIRED), + }, + ], + }, + tags: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.TAGS, + helpText: i18n.TAGS_HELP, + labelAppend: OptionalFieldLabel, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx new file mode 100644 index 0000000000000..44062a5a1d589 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem, EuiPanel, EuiTabbedContent, EuiTextArea } from '@elastic/eui'; +import React, { useState } from 'react'; +import styled from 'styled-components'; + +import { Markdown } from '../../../../components/markdown'; +import * as i18n from '../../translations'; +import { MarkdownHint } from '../../../../components/markdown/markdown_hint'; +import { CommonUseField } from '../create'; + +const TextArea = styled(EuiTextArea)<{ height: number }>` + min-height: ${({ height }) => `${height}px`}; + width: 100%; +`; + +TextArea.displayName = 'TextArea'; + +const DescriptionContainer = styled.div` + margin-top: 15px; + margin-bottom: 15px; +`; + +const DescriptionMarkdownTabs = styled(EuiTabbedContent)` + width: 100%; +`; + +DescriptionMarkdownTabs.displayName = 'DescriptionMarkdownTabs'; + +const MarkdownContainer = styled(EuiPanel)<{ height: number }>` + height: ${({ height }) => height}px; + overflow: auto; +`; + +MarkdownContainer.displayName = 'MarkdownContainer'; + +/** An input for entering a new case description */ +export const DescriptionMarkdown = React.memo<{ + descriptionInputHeight: number; + initialDescription: string; + isLoading: boolean; + formHook?: boolean; + onChange: (description: string) => void; +}>(({ initialDescription, isLoading, descriptionInputHeight, onChange, formHook = false }) => { + const [description, setDescription] = useState(initialDescription); + const tabs = [ + { + id: 'description', + name: i18n.DESCRIPTION, + content: formHook ? ( + { + setDescription(e as string); + onChange(e as string); + }} + componentProps={{ + idAria: 'caseDescription', + 'data-test-subj': 'caseDescription', + isDisabled: isLoading, + spellcheck: false, + }} + /> + ) : ( +