From c40b32010462aa6731d5c8a9312444228b3a819e Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 14 Nov 2019 16:50:23 -0600 Subject: [PATCH 01/44] [Logs UI] Add IE11-specific CSS fixes for anomalies table (#49980) * [Logs UI] Add IE11-specific CSS fixes for anomalies table * Switch to table-layout fix --- .../pages/logs/analysis/sections/anomalies/table.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx index ebf31d8320df5..76eb2fae77059 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx @@ -12,6 +12,7 @@ import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; import { LogRateResults } from '../../../../../containers/logs/log_analysis/log_analysis_results'; import { AnomaliesTableExpandedRow } from './expanded_row'; import { formatAnomalyScore, getFriendlyNameForPartitionId } from '../helpers/data_formatters'; +import euiStyled from '../../../../../../../../common/eui_styled_components'; interface TableItem { id: string; @@ -154,7 +155,7 @@ export const AnomaliesTable: React.FunctionComponent<{ ]; return ( - ); }; + +const StyledEuiBasicTable = euiStyled(EuiBasicTable)` + & .euiTable { + table-layout: auto; + } +`; From bd96d738dc36599d1b4a6ea69a9dbfac442f5442 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 14 Nov 2019 15:28:32 -0800 Subject: [PATCH 02/44] [DOCS] Adds documentation on telemetry settings (#50739) * [DOCS] Adds documentation on telemetry settings * [DOCS] Adds not that both settings can't be false at the same time --- docs/setup/settings.asciidoc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 11a50fea92f05..f4434ea7a09f4 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -320,6 +320,18 @@ supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2 setting this to `true` enables unauthenticated users to access the Kibana server status API and status page. +`telemetry.allowChangingOptInStatus`:: *Default: true*. If `true`, +users are able to change the telemetry setting at a later time in +<>. If `false`, +{kib} looks at the value of `telemetry.optIn` to determine whether to send +telemetry data or not. `telemetry.allowChangingOptInStatus` and `telemetry.optIn` +cannot be `false` at the same time. + +`telemetry.optIn`:: *Default: true* If `true`, telemetry data is sent to Elastic. + If `false`, collection of telemetry data is disabled. + To enable telemetry and prevent users from disabling it, + set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. + `vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. `xpack.license_management.enabled`:: *Default: true* Set this value to false to From 48a2156c4cd03b8bd523d5aff2bada12bb4e1ad1 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 15 Nov 2019 00:19:57 +0000 Subject: [PATCH 03/44] chore(NA): remove code plugin from codeowners (#50451) --- .github/CODEOWNERS | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 94296d076189b..0f1136fd5334b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,11 +28,6 @@ # Canvas /x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas -# Code -/x-pack/legacy/plugins/code/ @teams/code -/x-pack/test/functional/apps/code/ @teams/code -/x-pack/test/api_integration/apis/code/ @teams/code - # Logs & Metrics UI /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui /x-pack/legacy/plugins/integrations_manager/ @elastic/epm From 3131dd494d623c72690b4a6335f109d19e6d3cc3 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 14 Nov 2019 20:45:21 -0500 Subject: [PATCH 04/44] [SIEM] Add SavedQuery in Timeline (#49813) * Step-1: Add Search Bar in timeline instead of our own kql * Step-2: Add the saved query with filter in timeline savedObject * fix type * Fix unit test * fix bug when you use an exists filter * Fix bug to do a search when add filter by itself * Review I * unit tests * fix import for Filter * add range as a filter * remove comment * forget to add range in ES mapping + allow query with only filters * fix and/or with filter * review with Liza --- .../components/query_bar_top_row.tsx | 7 +- .../search_bar/components/search_bar.tsx | 20 +- .../common/es_query/filters/meta_filter.ts | 10 +- .../legacy/plugins/siem/common/constants.ts | 1 + .../legacy/plugins/siem/public/apps/index.ts | 5 +- .../plugins/siem/public/apps/plugin.tsx | 8 +- .../plugins/siem/public/apps/start_app.tsx | 32 +- .../public/components/flyout/index.test.tsx | 2 + .../components/flyout/pane/index.test.tsx | 9 +- .../components/open_timeline/helpers.test.ts | 161 +++++++ .../components/open_timeline/helpers.ts | 34 ++ .../page/hosts/hosts_table/index.test.tsx | 5 +- .../components/query_bar/index.test.tsx | 342 +++++++++++++++ .../public/components/query_bar/index.tsx | 146 +++++++ .../public/components/search_bar/index.tsx | 6 +- .../super_date_picker/index.test.tsx | 88 +--- .../components/super_date_picker/index.tsx | 16 +- .../timeline/fetch_kql_timeline.tsx | 1 - .../header/__snapshots__/index.test.tsx.snap | 1 + .../components/timeline/header/index.test.tsx | 9 + .../components/timeline/header/index.tsx | 6 +- .../components/timeline/helpers.test.tsx | 48 +++ .../public/components/timeline/helpers.tsx | 9 +- .../siem/public/components/timeline/index.tsx | 10 + .../timeline/properties/index.test.tsx | 8 + .../timeline/query_bar/index.test.tsx | 406 ++++++++++++++++++ .../components/timeline/query_bar/index.tsx | 310 +++++++++++++ .../timeline/search_or_filter/index.tsx | 190 ++++++-- .../search_or_filter/search_or_filter.tsx | 93 ++-- .../components/timeline/timeline.test.tsx | 13 + .../public/components/timeline/timeline.tsx | 5 +- .../timeline/one/index.gql_query.ts | 22 + .../containers/timeline/persist.gql_query.ts | 22 + .../siem/public/graphql/introspection.json | 376 +++++++++++++++- .../plugins/siem/public/graphql/types.ts | 196 ++++++++- .../plugins/siem/public/lib/keury/index.ts | 16 +- .../plugins/siem/public/mock/kibana_config.ts | 56 +++ .../siem/public/mock/test_providers.tsx | 60 ++- .../plugins/siem/public/mock/ui_settings.ts | 6 + .../pages/hosts/details/details_tabs.test.tsx | 5 +- .../siem/public/pages/hosts/hosts.test.tsx | 5 +- .../pages/network/ip_details/index.test.tsx | 5 +- .../public/pages/network/network.test.tsx | 5 +- .../siem/public/store/inputs/selectors.ts | 3 + .../legacy/plugins/siem/public/store/model.ts | 4 +- .../siem/public/store/timeline/actions.ts | 11 + .../siem/public/store/timeline/epic.test.ts | 283 ++++++++++++ .../siem/public/store/timeline/epic.ts | 89 +++- .../siem/public/store/timeline/helpers.ts | 42 ++ .../siem/public/store/timeline/model.ts | 5 + .../siem/public/store/timeline/reducer.ts | 26 +- .../public/utils/kql/use_update_kql.test.tsx | 7 - .../siem/public/utils/kql/use_update_kql.tsx | 8 +- .../utils/saved_query_services/index.tsx | 25 ++ .../server/graphql/timeline/schema.gql.ts | 54 ++- .../plugins/siem/server/graphql/types.ts | 284 +++++++++++- .../lib/timeline/saved_object_mappings.ts | 62 +++ .../plugins/siem/server/lib/timeline/types.ts | 29 ++ 58 files changed, 3450 insertions(+), 257 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx index ca0ac3c371849..cd64b1ecf2549 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx @@ -82,8 +82,11 @@ function QueryBarTopRowUI(props: Props) { const queryLanguage = props.query && props.query.language; const persistedLog: PersistedLog | undefined = React.useMemo( - () => (queryLanguage ? getQueryLog(uiSettings!, storage, appName, queryLanguage) : undefined), - [queryLanguage] + () => + queryLanguage && uiSettings && storage && appName + ? getQueryLog(uiSettings!, storage, appName, queryLanguage) + : undefined, + [appName, queryLanguage, uiSettings, storage] ); function onClickSubmitButton(event: React.MouseEvent) { diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index ea0f6775e4831..d713139366eef 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -72,6 +72,7 @@ export interface SearchBarOwnProps { // Show when user has privileges to save showSaveQuery?: boolean; savedQuery?: SavedQuery; + onQueryChange?: (payload: { dateRange: TimeRange; query?: Query }) => void; onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }) => void; // User has saved the current state as a saved query onSaved?: (savedQuery: SavedQuery) => void; @@ -206,6 +207,18 @@ class SearchBarUI extends Component { ); } + /* + * This Function is here to show the toggle in saved query form + * in case you the date range (from/to) + */ + private shouldRenderTimeFilterInSavedQueryForm() { + const { dateRangeFrom, dateRangeTo, showDatePicker } = this.props; + return ( + showDatePicker || + (!showDatePicker && dateRangeFrom !== undefined && dateRangeTo !== undefined) + ); + } + public setFilterBarHeight = () => { requestAnimationFrame(() => { const height = @@ -299,6 +312,9 @@ class SearchBarUI extends Component { dateRangeFrom: queryAndDateRange.dateRange.from, dateRangeTo: queryAndDateRange.dateRange.to, }); + if (this.props.onQueryChange) { + this.props.onQueryChange(queryAndDateRange); + } }; public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: Query }) => { @@ -440,7 +456,7 @@ class SearchBarUI extends Component { onSave={this.onSave} onClose={() => this.setState({ showSaveQueryModal: false })} showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.props.showDatePicker} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} /> ) : null} {this.state.showSaveNewQueryModal ? ( @@ -449,7 +465,7 @@ class SearchBarUI extends Component { onSave={savedQueryMeta => this.onSave(savedQueryMeta, true)} onClose={() => this.setState({ showSaveNewQueryModal: false })} showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.props.showDatePicker} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} /> ) : null} diff --git a/src/plugins/data/common/es_query/filters/meta_filter.ts b/src/plugins/data/common/es_query/filters/meta_filter.ts index 9adfdc4eedcb3..ff6dff9d8b749 100644 --- a/src/plugins/data/common/es_query/filters/meta_filter.ts +++ b/src/plugins/data/common/es_query/filters/meta_filter.ts @@ -33,15 +33,17 @@ export interface FilterValueFormatter { } export interface FilterMeta { + alias: string | null; + disabled: boolean; + negate: boolean; + // controlledBy is there to identify who owns the filter + controlledBy?: string; // index and type are optional only because when you create a new filter, there are no defaults index?: string; type?: string; - disabled: boolean; - negate: boolean; - alias: string | null; key?: string; - value?: string | ((formatter?: FilterValueFormatter) => string); params?: any; + value?: string | ((formatter?: FilterValueFormatter) => string); } export interface Filter { diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 6845648ee921d..2b4b4b78db626 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -25,6 +25,7 @@ export const DEFAULT_TO = 'now'; export const DEFAULT_INTERVAL_PAUSE = true; export const DEFAULT_INTERVAL_TYPE = 'manual'; export const DEFAULT_INTERVAL_VALUE = 300000; // ms +export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; /** * Id for the SIGNALS alerting type diff --git a/x-pack/legacy/plugins/siem/public/apps/index.ts b/x-pack/legacy/plugins/siem/public/apps/index.ts index 468e72c8a2e5c..b71c4fe699860 100644 --- a/x-pack/legacy/plugins/siem/public/apps/index.ts +++ b/x-pack/legacy/plugins/siem/public/apps/index.ts @@ -9,4 +9,7 @@ import { npStart } from 'ui/new_platform'; import { Plugin } from './plugin'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -new Plugin({ opaqueId: Symbol('siem'), env: {} as any }, chrome).start(npStart); +new Plugin({ opaqueId: Symbol('siem'), env: {} as any }, chrome).start( + npStart.core, + npStart.plugins +); diff --git a/x-pack/legacy/plugins/siem/public/apps/plugin.tsx b/x-pack/legacy/plugins/siem/public/apps/plugin.tsx index f3cbd44f34cdc..1f19841788ddb 100644 --- a/x-pack/legacy/plugins/siem/public/apps/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/apps/plugin.tsx @@ -15,11 +15,6 @@ import template from './template.html'; export const ROOT_ELEMENT_ID = 'react-siem-root'; -export interface StartObject { - core: LegacyCoreStart; - plugins: PluginsStart; -} - export class Plugin { constructor( // @ts-ignore this is added to satisfy the New Platform typing constraint, @@ -30,8 +25,7 @@ export class Plugin { this.chrome = chrome; } - public start(start: StartObject): void { - const { core, plugins } = start; + public start(core: LegacyCoreStart, plugins: PluginsStart) { // @ts-ignore improper type description this.chrome.setRootTemplate(template); const checkForRoot = () => { diff --git a/x-pack/legacy/plugins/siem/public/apps/start_app.tsx b/x-pack/legacy/plugins/siem/public/apps/start_app.tsx index 47c8b6eee6452..4549db946b815 100644 --- a/x-pack/legacy/plugins/siem/public/apps/start_app.tsx +++ b/x-pack/legacy/plugins/siem/public/apps/start_app.tsx @@ -9,6 +9,8 @@ import React, { memo, FC } from 'react'; import { ApolloProvider } from 'react-apollo'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { ThemeProvider } from 'styled-components'; +import { LegacyCoreStart } from 'kibana/public'; +import { PluginsStart } from 'ui/new_platform/new_platform'; import { EuiErrorBoundary } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; @@ -17,6 +19,9 @@ import { BehaviorSubject } from 'rxjs'; import { pluck } from 'rxjs/operators'; import { I18nContext } from 'ui/i18n'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; + import { DEFAULT_DARK_MODE } from '../../common/constants'; import { ErrorToastDispatcher } from '../components/error_toast_dispatcher'; import { compose } from '../lib/compose/kibana_compose'; @@ -31,8 +36,6 @@ import { MlCapabilitiesProvider } from '../components/ml/permissions/ml_capabili import { ApolloClientContext } from '../utils/apollo_context'; -import { StartObject } from './plugin'; - const StartApp: FC = memo(libs => { const history = createHashHistory(); @@ -74,10 +77,21 @@ const StartApp: FC = memo(libs => { export const ROOT_ELEMENT_ID = 'react-siem-root'; -export const SiemApp = memo(({ core, plugins }) => ( - - - - - -)); +export const SiemApp = memo<{ core: LegacyCoreStart; plugins: PluginsStart }>( + ({ core, plugins }) => ( + + + + + + + + ) +); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx index a7603762f424e..ddc3e4f15938a 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx @@ -20,6 +20,8 @@ import { FlyoutButton } from './button'; const testFlyoutHeight = 980; const usersViewing = ['elastic']; +jest.mock('../../lib/settings/use_kibana_ui_setting'); + describe('Flyout', () => { const state: State = mockGlobalState; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index 6f0f7a45e5c4e..6681e5a90b1a4 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -10,14 +10,21 @@ import 'jest-styled-components'; import * as React from 'react'; import { flyoutHeaderHeight } from '../'; +import { useKibanaCore } from '../../../lib/compose/kibana_core'; import { TestProviders } from '../../../mock'; - +import { mockUiSettings } from '../../../mock/ui_settings'; import { Pane } from '.'; const testFlyoutHeight = 980; const testWidth = 640; const usersViewing = ['elastic']; +const mockUseKibanaCore = useKibanaCore as jest.Mock; +jest.mock('../../../lib/compose/kibana_core'); +mockUseKibanaCore.mockImplementation(() => ({ + uiSettings: mockUiSettings, +})); + describe('Pane', () => { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts index fbd3f30049647..840d8c0a48124 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts @@ -235,6 +235,7 @@ describe('helpers', () => { }, description: '', eventIdToNoteIds: {}, + filters: [], highlightedDropAndProviderId: '', historyIds: [], id: 'savedObject-1', @@ -321,6 +322,7 @@ describe('helpers', () => { }, description: '', eventIdToNoteIds: {}, + filters: [], highlightedDropAndProviderId: '', historyIds: [], id: 'savedObject-1', @@ -400,6 +402,165 @@ describe('helpers', () => { dataProviders: [], description: '', eventIdToNoteIds: {}, + filters: [], + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + title: '', + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + dateRange: { + start: 0, + end: 0, + }, + show: false, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + width: 1100, + id: 'savedObject-1', + }); + }); + + test('should merge filters object back with json object', () => { + const timeline = { + savedObjectId: 'savedObject-1', + columns: timelineDefaults.columns.filter(column => column.id !== 'event.action'), + filters: [ + { + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: 'event.category', + negate: false, + params: '{"query":"file"}', + type: 'phrase', + value: null, + }, + query: '{"match_phrase":{"event.category":"file"}}', + exists: null, + }, + { + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: '@timestamp', + negate: false, + params: null, + type: 'exists', + value: 'exists', + }, + query: null, + exists: '{"field":"@timestamp"}', + }, + ], + version: '1', + }; + + const newTimeline = defaultTimelineToTimelineModel(timeline, false); + expect(newTimeline).toEqual({ + savedObjectId: 'savedObject-1', + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + version: '1', + dataProviders: [], + description: '', + eventIdToNoteIds: {}, + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + value: null, + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: 'appState', + }, + exists: { + field: '@timestamp', + }, + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: '@timestamp', + negate: false, + params: null, + type: 'exists', + value: 'exists', + }, + }, + ], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts index 1d2508fcfaf15..91480f20d8b00 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts @@ -61,6 +61,14 @@ const omitTypename = (key: string, value: keyof TimelineModel) => const omitTypenameInTimeline = (timeline: TimelineResult): TimelineResult => JSON.parse(JSON.stringify(timeline), omitTypename); +const parseString = (params: string) => { + try { + return JSON.parse(params); + } catch { + return params; + } +}; + export const defaultTimelineToTimelineModel = ( timeline: TimelineResult, duplicate: boolean @@ -97,6 +105,32 @@ export const defaultTimelineToTimelineModel = ( return acc; }, {}) : {}, + filters: + timeline.filters != null + ? timeline.filters.map(filter => ({ + $state: { + store: 'appState', + }, + meta: { + ...filter.meta, + ...(filter.meta && filter.meta.field != null + ? { params: parseString(filter.meta.field) } + : {}), + ...(filter.meta && filter.meta.params != null + ? { params: parseString(filter.meta.params) } + : {}), + ...(filter.meta && filter.meta.value != null + ? { value: parseString(filter.meta.value) } + : {}), + }, + ...(filter.exists != null ? { exists: parseString(filter.exists) } : {}), + ...(filter.match_all != null ? { exists: parseString(filter.match_all) } : {}), + ...(filter.missing != null ? { exists: parseString(filter.missing) } : {}), + ...(filter.query != null ? { query: parseString(filter.query) } : {}), + ...(filter.range != null ? { range: parseString(filter.range) } : {}), + ...(filter.script != null ? { exists: parseString(filter.script) } : {}), + })) + : [], isFavorite: duplicate ? false : timeline.favorite != null diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx index 44898e7a307ff..cdeaba0f067f5 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx @@ -30,10 +30,13 @@ mockUseKibanaCore.mockImplementation(() => ({ })); // Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar +// For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../../../query_bar', () => ({ + QueryBar: () => null, +})); describe('Hosts Table', () => { const loadPage = jest.fn(); diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx new file mode 100644 index 0000000000000..d619b515ccc7a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx @@ -0,0 +1,342 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { SearchBar } from '../../../../../../../src/legacy/core_plugins/data/public'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { uiSettingsServiceMock } from '../../../../../../../src/core/public/ui_settings/ui_settings_service.mock'; +import { useKibanaCore } from '../../lib/compose/kibana_core'; +import { TestProviders, mockIndexPattern } from '../../mock'; +import { QueryBar, QueryBarComponentProps } from '.'; +import { DEFAULT_FROM, DEFAULT_TO } from '../../../common/constants'; +import { mockUiSettings } from '../../mock/ui_settings'; + +jest.mock('ui/new_platform'); + +const mockUseKibanaCore = useKibanaCore as jest.Mock; +const mockUiSettingsForFilterManager = uiSettingsServiceMock.createSetupContract(); +jest.mock('../../lib/compose/kibana_core'); +mockUseKibanaCore.mockImplementation(() => ({ + uiSettings: mockUiSettings, + savedObjects: {}, +})); + +describe('QueryBar ', () => { + // We are doing that because we need to wrapped this component with redux + // and redux does not like to be updated and since we need to update our + // child component (BODY) and we do not want to scare anyone with this error + // we are hiding it!!! + // eslint-disable-next-line no-console + const originalError = console.error; + beforeAll(() => { + // eslint-disable-next-line no-console + console.error = (...args: string[]) => { + if (/ does not support changing `store` on the fly/.test(args[0])) { + return; + } + originalError.call(console, ...args); + }; + }); + + const mockOnChangeQuery = jest.fn(); + const mockOnSubmitQuery = jest.fn(); + const mockOnSavedQuery = jest.fn(); + + beforeEach(() => { + mockOnChangeQuery.mockClear(); + mockOnSubmitQuery.mockClear(); + mockOnSavedQuery.mockClear(); + }); + + test('check if we format the appropriate props to QueryBar', () => { + const wrapper = mount( + + + + ); + const { + customSubmitButton, + timeHistory, + onClearSavedQuery, + onFiltersUpdated, + onQueryChange, + onQuerySubmit, + onSaved, + onSavedQueryUpdated, + ...searchBarProps + } = wrapper.find(SearchBar).props(); + + expect(searchBarProps).toEqual({ + dateRangeFrom: 'now-24h', + dateRangeTo: 'now', + filters: [], + indexPatterns: [ + { + fields: [ + { + aggregatable: true, + name: '@timestamp', + searchable: true, + type: 'date', + }, + { + aggregatable: true, + name: '@version', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.id', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test1', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test2', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test3', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test4', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test5', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test6', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test7', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test8', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'host.name', + searchable: true, + type: 'string', + }, + ], + title: 'filebeat-*,auditbeat-*,packetbeat-*', + }, + ], + isRefreshPaused: true, + query: { + language: 'kuery', + query: 'here: query', + }, + refreshInterval: undefined, + showAutoRefreshOnly: false, + showDatePicker: false, + showFilterBar: true, + showQueryBar: true, + showQueryInput: true, + showSaveQuery: true, + }); + }); + + describe('#onQueryChange', () => { + test(' is the only reference that changed when filterQueryDraft props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]'); + queryInput.simulate('change', { target: { value: 'hello: world' } }); + wrapper.update(); + + expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + }); + + describe('#onQuerySubmit', () => { + test(' is the only reference that changed when filterQuery props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + + test(' is only reference that changed when timelineId props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ onSubmitQuery: jest.fn() }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + }); + + describe('#onSavedQueryUpdated', () => { + test('is only reference that changed when dataProviders props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ onSavedQuery: jest.fn() }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx new file mode 100644 index 0000000000000..c7e58532fc7e5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx @@ -0,0 +1,146 @@ +/* + * 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 { isEqual } from 'lodash/fp'; +import React, { memo, useState, useEffect, useMemo, useCallback } from 'react'; +import { StaticIndexPattern, IndexPattern } from 'ui/index_patterns'; + +import { SavedQuery, SearchBar } from '../../../../../../../src/legacy/core_plugins/data/public'; +import { + esFilters, + FilterManager, + Query, + TimeHistory, + TimeRange, +} from '../../../../../../../src/plugins/data/public'; +import { SavedQueryTimeFilter } from '../../../../../../../src/legacy/core_plugins/data/public/search'; +import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; + +export interface QueryBarComponentProps { + dateRangeFrom?: string; + dateRangeTo?: string; + hideSavedQuery?: boolean; + indexPattern: StaticIndexPattern; + isRefreshPaused?: boolean; + filterQuery: Query; + filterManager: FilterManager; + filters: esFilters.Filter[]; + onChangedQuery: (query: Query) => void; + onSubmitQuery: (query: Query, timefilter?: SavedQueryTimeFilter) => void; + refreshInterval?: number; + savedQuery?: SavedQuery | null; + onSavedQuery: (savedQuery: SavedQuery | null) => void; +} + +export const QueryBar = memo( + ({ + dateRangeFrom, + dateRangeTo, + hideSavedQuery = false, + indexPattern, + isRefreshPaused, + filterQuery, + filterManager, + filters, + onChangedQuery, + onSubmitQuery, + refreshInterval, + savedQuery, + onSavedQuery, + }) => { + const [draftQuery, setDraftQuery] = useState(filterQuery); + + useEffect(() => { + setDraftQuery(filterQuery); + }, [filterQuery]); + + const onQuerySubmit = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + if (payload.query != null && !isEqual(payload.query, filterQuery)) { + onSubmitQuery(payload.query); + } + }, + [filterQuery, onSubmitQuery] + ); + + const onQueryChange = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + if (payload.query != null && !isEqual(payload.query, draftQuery)) { + setDraftQuery(payload.query); + onChangedQuery(payload.query); + } + }, + [draftQuery, onChangedQuery, setDraftQuery] + ); + + const onSaved = useCallback( + (newSavedQuery: SavedQuery) => { + onSavedQuery(newSavedQuery); + }, + [onSavedQuery] + ); + + const onSavedQueryUpdated = useCallback( + (savedQueryUpdated: SavedQuery) => { + const { query: newQuery, filters: newFilters, timefilter } = savedQueryUpdated.attributes; + onSubmitQuery(newQuery, timefilter); + filterManager.setFilters(newFilters || []); + onSavedQuery(savedQueryUpdated); + }, + [filterManager, onSubmitQuery, onSavedQuery] + ); + + const onClearSavedQuery = useCallback(() => { + if (savedQuery != null) { + onSubmitQuery({ + query: '', + language: savedQuery.attributes.query.language, + }); + filterManager.setFilters([]); + onSavedQuery(null); + } + }, [filterManager, onSubmitQuery, onSavedQuery, savedQuery]); + + const onFiltersUpdated = useCallback( + (newFilters: esFilters.Filter[]) => { + filterManager.setFilters(newFilters); + }, + [filterManager] + ); + + const CustomButton = <>{null}; + const indexPatterns = useMemo(() => [indexPattern as IndexPattern], [indexPattern]); + + const searchBarProps = savedQuery != null ? { savedQuery } : {}; + + return ( + + ); + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx index f5a99c631131f..850d78e1c3428 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx @@ -234,7 +234,7 @@ const SearchBarComponent = memo { let isSubscribed = true; @@ -258,13 +258,13 @@ const SearchBarComponent = memo [indexPattern as IndexPattern], [indexPattern]); + const indexPatterns = useMemo(() => [indexPattern as IndexPattern], [indexPattern]); return ( { describe('#SuperDatePicker', () => { const state: State = mockGlobalState; @@ -74,38 +76,6 @@ describe('SIEM Super Date Picker', () => { expect(store.getState().inputs.global.timerange.toStr).toBe('now/d'); }); - test('Make Sure it is this week', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_This_week"]') - .first() - .simulate('click'); - wrapper.update(); - expect(store.getState().inputs.global.timerange.fromStr).toBe('now/w'); - expect(store.getState().inputs.global.timerange.toStr).toBe('now/w'); - }); - - test('Make Sure it is week to date', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Week_to date"]') - .first() - .simulate('click'); - wrapper.update(); - expect(store.getState().inputs.global.timerange.fromStr).toBe('now/w'); - expect(store.getState().inputs.global.timerange.toStr).toBe('now'); - }); - test('Make Sure to (end date) is superior than from (start date)', () => { expect(store.getState().inputs.global.timerange.to).toBeGreaterThan( store.getState().inputs.global.timerange.from @@ -168,60 +138,6 @@ describe('SIEM Super Date Picker', () => { ).toBe('Last 15 minutesToday'); }); - test('Today and Year to date is in Recently used date ranges', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Year_to date"]') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('div.euiQuickSelectPopover__section') - .at(1) - .text() - ).toBe('Year to dateToday'); - }); - - test('Today and Last 15 minutes and Year to date is in Recently used date ranges', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('button.euiQuickSelect__applyButton') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Year_to date"]') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('div.euiQuickSelectPopover__section') - .at(1) - .text() - ).toBe('Year to dateLast 15 minutesToday'); - }); - test('Make sure that it does not add any duplicate if you click again on today', () => { wrapper .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx index caeb29fc6de7d..a2e190da0f7bc 100644 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx @@ -12,11 +12,13 @@ import { OnRefreshProps, OnTimeChangeProps, } from '@elastic/eui'; -import { getOr, take } from 'lodash/fp'; +import { getOr, take, isEmpty } from 'lodash/fp'; import React, { useState, useCallback } from 'react'; import { connect } from 'react-redux'; - import { Dispatch } from 'redux'; + +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../common/constants'; +import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting'; import { inputsModel, State } from '../../store'; import { inputsActions, timelineActions } from '../../store/actions'; import { InputsModelId } from '../../store/inputs/constants'; @@ -194,8 +196,18 @@ export const SuperDatePickerComponent = React.memo( const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); + const [quickRanges] = useKibanaUiSetting(DEFAULT_TIMEPICKER_QUICK_RANGES); + const commonlyUsedRanges = isEmpty(quickRanges) + ? [] + : quickRanges.map(({ from, to, display }: { from: string; to: string; display: string }) => ({ + start: from, + end: to, + label: display, + })); + return ( ( kueryFilterQuery, kueryFilterQueryDraft, storeType: 'timelineType', - type: null, timelineId: id, }), }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap index a2d20ff0b8d18..048ca080772f6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -150,6 +150,7 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` show={true} /> ({ + uiSettings: mockUiSettings, + savedObjects: {}, +})); + describe('Header', () => { const indexPattern = mockIndexPattern; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx index f7802203d1258..9377668b4fdaf 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx @@ -84,7 +84,11 @@ export const TimelineHeader = React.memo( onToggleDataProviderExcluded={onToggleDataProviderExcluded} show={show} /> - + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx index b30771760bad3..7664814f71147 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx @@ -6,6 +6,8 @@ import { cloneDeep } from 'lodash/fp'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; +import { FilterStateStore } from '../../../../../../../src/plugins/data/common/es_query/filters'; import { mockIndexPattern } from '../../mock'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; @@ -160,6 +162,52 @@ describe('Combined Queries', () => { }); }); + test('No Data Provider & No kqlQuery & with Filters', () => { + const isEventViewer = true; + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { query: 'file' }, + type: 'phrase', + }, + query: { match_phrase: { 'event.category': 'file' } }, + }, + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'host.name', + negate: false, + type: 'exists', + value: 'exists', + }, + exists: { field: 'host.name' }, + } as esFilters.Filter, + ], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + isEventViewer, + }) + ).toEqual({ + filterQuery: + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', + }); + }); + test('Only Data Provider', () => { const dataProviders = mockDataProviders.slice(0, 1); const { filterQuery } = combineQueries({ diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx index 6182fca6e2e99..e31f5aac137b8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx @@ -7,7 +7,7 @@ import { isEmpty, isNumber, get } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import { StaticIndexPattern } from 'ui/index_patterns'; -import { Query, esFilters } from 'src/plugins/data/public'; +import { Query, esFilters } from '../../../../../../../src/plugins/data/public'; import { escapeQueryValue, convertToBuildEsQuery, EsQueryConfig } from '../../lib/keury'; @@ -113,13 +113,18 @@ export const combineQueries = ({ isEventViewer?: boolean; }): { filterQuery: string } | null => { const kuery: Query = { query: '', language: kqlQuery.language }; - if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEventViewer) { + if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { return null; } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; + } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { + kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { kuery.query = `(${kqlQuery.query}) and @timestamp >= ${start} and @timestamp <= ${end}`; return { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index e4afef9a351e8..8c911b4ab06cb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -9,6 +9,8 @@ import React, { useEffect, useCallback } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; + import { WithSource } from '../../containers/source'; import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; import { timelineActions } from '../../store/actions'; @@ -40,6 +42,7 @@ interface StateReduxProps { columns: ColumnHeader[]; dataProviders?: DataProvider[]; end: number; + filters: esFilters.Filter[]; isLive: boolean; itemsPerPage?: number; itemsPerPageOptions?: number[]; @@ -137,6 +140,7 @@ const StatefulTimelineComponent = React.memo( createTimeline, dataProviders, end, + filters, flyoutHeaderHeight, flyoutHeight, id, @@ -252,6 +256,7 @@ const StatefulTimelineComponent = React.memo( columns={columns} dataProviders={dataProviders!} end={end} + filters={filters} flyoutHeaderHeight={flyoutHeaderHeight} flyoutHeight={flyoutHeight} id={id} @@ -295,6 +300,7 @@ const StatefulTimelineComponent = React.memo( prevProps.start === nextProps.start && isEqual(prevProps.columns, nextProps.columns) && isEqual(prevProps.dataProviders, nextProps.dataProviders) && + isEqual(prevProps.filters, nextProps.filters) && isEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && isEqual(prevProps.sort, nextProps.sort) ); @@ -314,6 +320,7 @@ const makeMapStateToProps = () => { const { columns, dataProviders, + filters, itemsPerPage, itemsPerPageOptions, kqlMode, @@ -322,10 +329,13 @@ const makeMapStateToProps = () => { } = timeline; const kqlQueryExpression = getKqlQueryTimeline(state, id); + const timelineFilter = kqlMode === 'filter' ? filters || [] : []; + return { columns, dataProviders, end: input.timerange.to, + filters: timelineFilter, id, isLive: input.policy.kind === 'interval', itemsPerPage, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx index 3b42ead64ad01..8c586cf958417 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx @@ -8,11 +8,19 @@ import { mount } from 'enzyme'; import * as React from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; +import { useKibanaCore } from '../../../lib/compose/kibana_core'; import { mockGlobalState, apolloClientObservable } from '../../../mock'; +import { mockUiSettings } from '../../../mock/ui_settings'; import { createStore, State } from '../../../store'; import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; +const mockUseKibanaCore = useKibanaCore as jest.Mock; +jest.mock('../../../lib/compose/kibana_core'); +mockUseKibanaCore.mockImplementation(() => ({ + uiSettings: mockUiSettings, +})); + describe('Properties', () => { const usersViewing = ['elastic']; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx new file mode 100644 index 0000000000000..b78691fabdcbf --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx @@ -0,0 +1,406 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants'; +import { mockBrowserFields } from '../../../containers/source/mock'; +import { useKibanaCore } from '../../../lib/compose/kibana_core'; +import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; +import { mockIndexPattern, TestProviders } from '../../../mock'; +import { mockUiSettings } from '../../../mock/ui_settings'; +import { QueryBar } from '../../query_bar'; +import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; +import { buildGlobalQuery } from '../helpers'; + +import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; + +const mockUseKibanaCore = useKibanaCore as jest.Mock; +jest.mock('../../../lib/compose/kibana_core'); +mockUseKibanaCore.mockImplementation(() => ({ + uiSettings: mockUiSettings, + savedObjects: {}, +})); + +describe('Timeline QueryBar ', () => { + // We are doing that because we need to wrapped this component with redux + // and redux does not like to be updated and since we need to update our + // child component (BODY) and we do not want to scare anyone with this error + // we are hiding it!!! + // eslint-disable-next-line no-console + const originalError = console.error; + beforeAll(() => { + // eslint-disable-next-line no-console + console.error = (...args: string[]) => { + if (/ does not support changing `store` on the fly/.test(args[0])) { + return; + } + originalError.call(console, ...args); + }; + }); + + const mockApplyKqlFilterQuery = jest.fn(); + const mockSetFilters = jest.fn(); + const mockSetKqlFilterQueryDraft = jest.fn(); + const mockSetSavedQueryId = jest.fn(); + const mockUpdateReduxTime = jest.fn(); + + beforeEach(() => { + mockApplyKqlFilterQuery.mockClear(); + mockSetFilters.mockClear(); + mockSetKqlFilterQueryDraft.mockClear(); + mockSetSavedQueryId.mockClear(); + mockUpdateReduxTime.mockClear(); + }); + + test('check if we format the appropriate props to QueryBar', () => { + const wrapper = mount( + + + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + + expect(queryBarProps.dateRangeFrom).toEqual('now-24h'); + expect(queryBarProps.dateRangeTo).toEqual('now'); + expect(queryBarProps.filterQuery).toEqual({ query: 'here: query', language: 'kuery' }); + expect(queryBarProps.savedQuery).toEqual(null); + }); + + describe('#onChangeQuery', () => { + test(' is the only reference that changed when filterQueryDraft props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ filterQueryDraft: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onChangedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + }); + + describe('#onSubmitQuery', () => { + test(' is the only reference that changed when filterQuery props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + + test(' is only reference that changed when timelineId props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ timelineId: 'new-timeline' }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + }); + + describe('#onSavedQuery', () => { + test('is only reference that changed when dataProviders props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ dataProviders: mockDataProviders.slice(1, 0) }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + }); + + test('is only reference that changed when savedQueryId props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ + savedQueryId: 'new', + }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + }); + }); + + describe('#getDataProviderFilter', () => { + test('returns valid data provider filter with a simple bool data provider', () => { + const dataProvidersDsl = convertKueryToElasticSearchQuery( + buildGlobalQuery(mockDataProviders.slice(0, 1), mockBrowserFields), + mockIndexPattern + ); + const filter = getDataProviderFilter(dataProvidersDsl); + expect(filter).toEqual({ + $state: { + store: 'appState', + }, + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + name: 'Provider 1', + }, + }, + ], + }, + meta: { + alias: 'timeline-filter-drop-area', + controlledBy: 'timeline-filter-drop-area', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}', + }, + }); + }); + + test('returns valid data provider filter with an exists operator', () => { + const dataProvidersDsl = convertKueryToElasticSearchQuery( + buildGlobalQuery( + [ + { + id: `id-exists`, + name, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: '', + operator: ':*', + }, + and: [], + }, + ], + mockBrowserFields + ), + mockIndexPattern + ); + const filter = getDataProviderFilter(dataProvidersDsl); + expect(filter).toEqual({ + $state: { + store: 'appState', + }, + bool: { + minimum_should_match: 1, + should: [ + { + exists: { + field: 'host.name', + }, + }, + ], + }, + meta: { + alias: 'timeline-filter-drop-area', + controlledBy: 'timeline-filter-drop-area', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', + }, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx new file mode 100644 index 0000000000000..cb352059aaca4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx @@ -0,0 +1,310 @@ +/* + * 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 { isEqual, isEmpty } from 'lodash/fp'; +import React, { memo, useCallback, useState, useEffect } from 'react'; +import { StaticIndexPattern } from 'ui/index_patterns'; +import { Query } from 'src/plugins/data/common/types'; +import { Subscription } from 'rxjs'; + +import { SavedQueryTimeFilter } from '../../../../../../../../src/legacy/core_plugins/data/public/search'; +import { SavedQuery } from '../../../../../../../../src/legacy/core_plugins/data/public'; +import { FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { + Filter, + FilterStateStore, +} from '../../../../../../../../src/plugins/data/common/es_query/filters'; + +import { BrowserFields } from '../../../containers/source'; +import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; +import { useKibanaCore } from '../../../lib/compose/kibana_core'; +import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; +import { KqlMode } from '../../../store/timeline/model'; +import { useSavedQueryServices } from '../../../utils/saved_query_services'; +import { DispatchUpdateReduxTime } from '../../super_date_picker'; +import { QueryBar } from '../../query_bar'; +import { DataProvider } from '../data_providers/data_provider'; +import { buildGlobalQuery } from '../helpers'; + +export interface QueryBarTimelineComponentProps { + applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; + browserFields: BrowserFields; + dataProviders: DataProvider[]; + filters: Filter[]; + filterQuery: KueryFilterQuery; + filterQueryDraft: KueryFilterQuery; + from: number; + fromStr: string; + kqlMode: KqlMode; + indexPattern: StaticIndexPattern; + isRefreshPaused: boolean; + refreshInterval: number; + savedQueryId: string | null; + setFilters: (filters: Filter[]) => void; + setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; + setSavedQueryId: (savedQueryId: string | null) => void; + timelineId: string; + to: number; + toStr: string; + updateReduxTime: DispatchUpdateReduxTime; +} + +const timelineFilterDropArea = 'timeline-filter-drop-area'; + +export const QueryBarTimeline = memo( + ({ + applyKqlFilterQuery, + browserFields, + dataProviders, + filters, + filterQuery, + filterQueryDraft, + from, + fromStr, + kqlMode, + indexPattern, + isRefreshPaused, + savedQueryId, + setFilters, + setKqlFilterQueryDraft, + setSavedQueryId, + refreshInterval, + timelineId, + to, + toStr, + updateReduxTime, + }) => { + const [dateRangeFrom, setDateRangeFrom] = useState( + fromStr != null ? fromStr : new Date(from).toISOString() + ); + const [dateRangeTo, setDateRangTo] = useState( + toStr != null ? toStr : new Date(to).toISOString() + ); + + const [savedQuery, setSavedQuery] = useState(null); + const [filterQueryConverted, setFilterQueryConverted] = useState({ + query: filterQuery != null ? filterQuery.expression : '', + language: filterQuery != null ? filterQuery.kind : 'kuery', + }); + const [queryBarFilters, setQueryBarFilters] = useState([]); + const [dataProvidersDsl, setDataProvidersDsl] = useState( + convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) + ); + const core = useKibanaCore(); + const [filterManager] = useState(new FilterManager(core.uiSettings)); + + const savedQueryServices = useSavedQueryServices(); + + useEffect(() => { + let isSubscribed = true; + const subscriptions = new Subscription(); + filterManager.setFilters(filters); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + if (isSubscribed) { + const filterWithoutDropArea = filterManager + .getFilters() + .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + setFilters(filterWithoutDropArea); + setQueryBarFilters(filterWithoutDropArea); + } + }, + }) + ); + + return () => { + isSubscribed = false; + subscriptions.unsubscribe(); + }; + }, []); + + useEffect(() => { + const filterWithoutDropArea = filterManager + .getFilters() + .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + if (!isEqual(filters, filterWithoutDropArea)) { + filterManager.setFilters(filters); + } + }, [filters]); + + useEffect(() => { + setFilterQueryConverted({ + query: filterQuery != null ? filterQuery.expression : '', + language: filterQuery != null ? filterQuery.kind : 'kuery', + }); + }, [filterQuery]); + + useEffect(() => { + setDataProvidersDsl( + convertKueryToElasticSearchQuery( + buildGlobalQuery(dataProviders, browserFields), + indexPattern + ) + ); + }, [dataProviders, browserFields, indexPattern]); + + useEffect(() => { + if (fromStr != null && toStr != null) { + setDateRangeFrom(fromStr); + setDateRangTo(toStr); + } else if (from != null && to != null) { + setDateRangeFrom(new Date(from).toISOString()); + setDateRangTo(new Date(to).toISOString()); + } + }, [from, fromStr, to, toStr]); + + useEffect(() => { + let isSubscribed = true; + async function setSavedQueryByServices() { + if (savedQueryId != null && savedQueryServices != null) { + const mySavedQuery = await savedQueryServices.getSavedQuery(savedQueryId); + if (isSubscribed) { + setSavedQuery({ + ...mySavedQuery, + attributes: { + ...mySavedQuery.attributes, + filters: filters.filter(f => f.meta.controlledBy !== timelineFilterDropArea), + }, + }); + } + } else if (isSubscribed) { + setSavedQuery(null); + } + } + setSavedQueryByServices(); + return () => { + isSubscribed = false; + }; + }, [savedQueryId]); + + const onChangedQuery = useCallback( + (newQuery: Query) => { + if ( + filterQueryDraft == null || + (filterQueryDraft != null && filterQueryDraft.expression !== newQuery.query) || + filterQueryDraft.kind !== newQuery.language + ) { + setKqlFilterQueryDraft( + newQuery.query as string, + newQuery.language as KueryFilterQueryKind + ); + } + }, + [filterQueryDraft] + ); + + const onSubmitQuery = useCallback( + (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { + if ( + filterQuery == null || + (filterQuery != null && filterQuery.expression !== newQuery.query) || + filterQuery.kind !== newQuery.language + ) { + setKqlFilterQueryDraft( + newQuery.query as string, + newQuery.language as KueryFilterQueryKind + ); + applyKqlFilterQuery(newQuery.query as string, newQuery.language as KueryFilterQueryKind); + } + if (timefilter != null) { + const isQuickSelection = timefilter.from.includes('now') || timefilter.to.includes('now'); + + updateReduxTime({ + id: 'timeline', + end: timefilter.to, + start: timefilter.from, + isInvalid: false, + isQuickSelection, + timelineId, + }); + } + }, + [filterQuery, timelineId] + ); + + const onSavedQuery = useCallback( + (newSavedQuery: SavedQuery | null) => { + if (newSavedQuery != null) { + if (newSavedQuery.id !== savedQueryId) { + setSavedQueryId(newSavedQuery.id); + } + if (savedQueryServices != null && dataProvidersDsl !== '') { + const dataProviderFilterExists = + newSavedQuery.attributes.filters != null + ? newSavedQuery.attributes.filters.findIndex( + f => f.meta.controlledBy === timelineFilterDropArea + ) + : -1; + savedQueryServices.saveQuery( + { + ...newSavedQuery.attributes, + filters: + newSavedQuery.attributes.filters != null + ? dataProviderFilterExists > -1 + ? [ + ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), + getDataProviderFilter(dataProvidersDsl), + ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), + ] + : [ + ...newSavedQuery.attributes.filters, + getDataProviderFilter(dataProvidersDsl), + ] + : [], + }, + { + overwrite: true, + } + ); + } + } else { + setSavedQueryId(null); + } + }, + [dataProvidersDsl, savedQueryId, savedQueryServices] + ); + + return ( + + ); + } +); + +export const getDataProviderFilter = (dataProviderDsl: string): Filter => { + const dslObject = JSON.parse(dataProviderDsl); + const key = Object.keys(dslObject); + return { + ...dslObject, + meta: { + alias: timelineFilterDropArea, + controlledBy: timelineFilterDropArea, + negate: false, + disabled: false, + type: 'custom', + key: isEmpty(key) ? 'bool' : key[0], + value: dataProviderDsl, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx index ec491fe50407a..4af6178a7223b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -4,43 +4,69 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; +import { getOr, isEqual } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect } from 'react-redux'; -import { ActionCreator } from 'typescript-fsa'; +import { Dispatch } from 'redux'; import { StaticIndexPattern } from 'ui/index_patterns'; +import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { BrowserFields } from '../../../containers/source'; import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { KueryFilterQuery, SerializedFilterQuery, State, timelineSelectors } from '../../../store'; - -import { SearchOrFilter } from './search_or_filter'; +import { + KueryFilterQuery, + SerializedFilterQuery, + State, + timelineSelectors, + inputsModel, + inputsSelectors, +} from '../../../store'; import { timelineActions } from '../../../store/actions'; import { KqlMode, TimelineModel } from '../../../store/timeline/model'; +import { DispatchUpdateReduxTime, dispatchUpdateReduxTime } from '../../super_date_picker'; +import { DataProvider } from '../data_providers/data_provider'; +import { SearchOrFilter } from './search_or_filter'; interface OwnProps { + browserFields: BrowserFields; indexPattern: StaticIndexPattern; timelineId: string; } interface StateReduxProps { + dataProviders: DataProvider[]; + filters: esFilters.Filter[]; + filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; - isFilterQueryDraftValid: boolean; - kqlMode?: KqlMode; + from: number; + fromStr: string; + isRefreshPaused: boolean; + kqlMode: KqlMode; + refreshInterval: number; + savedQueryId: string | null; + to: number; + toStr: string; } interface DispatchProps { - applyKqlFilterQuery: ActionCreator<{ + applyKqlFilterQuery: ({ + id, + filterQuery, + }: { id: string; filterQuery: SerializedFilterQuery; - }>; - updateKqlMode: ActionCreator<{ - id: string; - kqlMode: KqlMode; - }>; - setKqlFilterQueryDraft: ActionCreator<{ + }) => void; + updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => void; + setKqlFilterQueryDraft: ({ + id, + filterQueryDraft, + }: { id: string; filterQueryDraft: KueryFilterQuery; - }>; + }) => void; + setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => void; + setFilters: ({ id, filters }: { id: string; filters: esFilters.Filter[] }) => void; + updateReduxTime: DispatchUpdateReduxTime; } type Props = OwnProps & StateReduxProps & DispatchProps; @@ -48,21 +74,34 @@ type Props = OwnProps & StateReduxProps & DispatchProps; const StatefulSearchOrFilterComponent = React.memo( ({ applyKqlFilterQuery, + browserFields, + dataProviders, + filters, + filterQuery, filterQueryDraft, + from, + fromStr, indexPattern, - isFilterQueryDraftValid, + isRefreshPaused, kqlMode, + refreshInterval, + savedQueryId, + setFilters, setKqlFilterQueryDraft, + setSavedQueryId, timelineId, + to, + toStr, updateKqlMode, + updateReduxTime, }) => { const applyFilterQueryFromKueryExpression = useCallback( - (expression: string) => + (expression: string, kind) => applyKqlFilterQuery({ id: timelineId, filterQuery: { kuery: { - kind: 'kuery', + kind, expression, }, serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), @@ -72,29 +111,80 @@ const StatefulSearchOrFilterComponent = React.memo( ); const setFilterQueryDraftFromKueryExpression = useCallback( - (expression: string) => + (expression: string, kind) => setKqlFilterQueryDraft({ id: timelineId, filterQueryDraft: { - kind: 'kuery', + kind, expression, }, }), [timelineId] ); + const setFiltersInTimeline = useCallback( + (newFilters: esFilters.Filter[]) => + setFilters({ + id: timelineId, + filters: newFilters, + }), + [timelineId] + ); + + const setSavedQueryInTimeline = useCallback( + (newSavedQueryId: string | null) => + setSavedQueryId({ + id: timelineId, + savedQueryId: newSavedQueryId, + }), + [timelineId] + ); + return ( ); + }, + (prevProps, nextProps) => { + return ( + prevProps.from === nextProps.from && + prevProps.fromStr === nextProps.fromStr && + prevProps.to === nextProps.to && + prevProps.toStr === nextProps.toStr && + prevProps.isRefreshPaused === nextProps.isRefreshPaused && + prevProps.refreshInterval === nextProps.refreshInterval && + prevProps.timelineId === nextProps.timelineId && + isEqual(prevProps.browserFields, nextProps.browserFields) && + isEqual(prevProps.dataProviders, nextProps.dataProviders) && + isEqual(prevProps.filters, nextProps.filters) && + isEqual(prevProps.filterQuery, nextProps.filterQuery) && + isEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && + isEqual(prevProps.indexPattern, nextProps.indexPattern) && + isEqual(prevProps.kqlMode, nextProps.kqlMode) && + isEqual(prevProps.savedQueryId, nextProps.savedQueryId) && + isEqual(prevProps.timelineId, nextProps.timelineId) + ); } ); StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent'; @@ -102,20 +192,62 @@ StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent'; const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const getKqlFilterQueryDraft = timelineSelectors.getKqlFilterQueryDraftSelector(); - const isFilterQueryDraftValid = timelineSelectors.isFilterQueryDraftValidSelector(); + const getKqlFilterQuery = timelineSelectors.getKqlFilterKuerySelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getInputsPolicy = inputsSelectors.getTimelinePolicySelector(); const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel | {} = getTimeline(state, timelineId); + const timeline: TimelineModel = getTimeline(state, timelineId); + const input: inputsModel.InputsRange = getInputsTimeline(state); + const policy: inputsModel.Policy = getInputsPolicy(state); return { + dataProviders: timeline.dataProviders, + filterQuery: getKqlFilterQuery(state, timelineId), filterQueryDraft: getKqlFilterQueryDraft(state, timelineId), - isFilterQueryDraftValid: isFilterQueryDraftValid(state, timelineId), + filters: timeline.filters, + from: input.timerange.from, + fromStr: input.timerange.fromStr, + isRefreshPaused: policy.kind === 'manual', kqlMode: getOr('filter', 'kqlMode', timeline), + refreshInterval: policy.duration, + savedQueryId: getOr(null, 'savedQueryId', timeline), + to: input.timerange.to, + toStr: input.timerange.toStr, }; }; return mapStateToProps; }; -export const StatefulSearchOrFilter = connect(makeMapStateToProps, { - applyKqlFilterQuery: timelineActions.applyKqlFilterQuery, - setKqlFilterQueryDraft: timelineActions.setKqlFilterQueryDraft, - updateKqlMode: timelineActions.updateKqlMode, -})(StatefulSearchOrFilterComponent); +const mapDispatchToProps = (dispatch: Dispatch) => ({ + applyKqlFilterQuery: ({ id, filterQuery }: { id: string; filterQuery: SerializedFilterQuery }) => + dispatch( + timelineActions.applyKqlFilterQuery({ + id, + filterQuery, + }) + ), + updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => + dispatch(timelineActions.updateKqlMode({ id, kqlMode })), + setKqlFilterQueryDraft: ({ + id, + filterQueryDraft, + }: { + id: string; + filterQueryDraft: KueryFilterQuery; + }) => + dispatch( + timelineActions.setKqlFilterQueryDraft({ + id, + filterQueryDraft, + }) + ), + setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => + dispatch(timelineActions.setSavedQueryId({ id, savedQueryId })), + setFilters: ({ id, filters }: { id: string; filters: esFilters.Filter[] }) => + dispatch(timelineActions.setFilters({ id, filters })), + updateReduxTime: dispatchUpdateReduxTime(dispatch), +}); + +export const StatefulSearchOrFilter = connect( + makeMapStateToProps, + mapDispatchToProps +)(StatefulSearchOrFilterComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 08ab44317e82f..db8909adda239 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -10,13 +10,16 @@ import { pure } from 'recompose'; import styled, { injectGlobal } from 'styled-components'; import { StaticIndexPattern } from 'ui/index_patterns'; -import { KueryAutocompletion } from '../../../containers/kuery_autocompletion'; -import { KueryFilterQuery } from '../../../store'; -import { AutocompleteField } from '../../autocomplete_field'; +import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { BrowserFields } from '../../../containers/source'; +import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; +import { KqlMode } from '../../../store/timeline/model'; +import { DataProvider } from '../data_providers/data_provider'; +import { QueryBarTimeline } from '../query_bar'; -import { getPlaceholderText, modes, options } from './helpers'; +import { options } from './helpers'; import * as i18n from './translations'; -import { KqlMode } from '../../../store/timeline/model'; +import { DispatchUpdateReduxTime } from '../../super_date_picker'; const timelineSelectModeItemsClassName = 'timelineSelectModeItemsClassName'; const searchOrFilterPopoverClassName = 'searchOrFilterPopover'; @@ -39,19 +42,40 @@ injectGlobal` `; interface Props { - applyKqlFilterQuery: (expression: string) => void; + applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; + browserFields: BrowserFields; + dataProviders: DataProvider[]; + filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; + from: number; + fromStr: string; indexPattern: StaticIndexPattern; - isFilterQueryDraftValid: boolean; + isRefreshPaused: boolean; kqlMode: KqlMode; timelineId: string; updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => void; - setKqlFilterQueryDraft: (expression: string) => void; + refreshInterval: number; + setFilters: (filters: esFilters.Filter[]) => void; + setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; + setSavedQueryId: (savedQueryId: string | null) => void; + filters: esFilters.Filter[]; + savedQueryId: string | null; + to: number; + toStr: string; + updateReduxTime: DispatchUpdateReduxTime; } const SearchOrFilterContainer = styled.div` margin: 5px 0 10px 0; user-select: none; + .globalQueryBar { + padding: 0px; + .kbnQueryBar { + div:first-child { + margin-right: 0px; + } + } + } `; SearchOrFilterContainer.displayName = 'SearchOrFilterContainer'; @@ -65,13 +89,26 @@ ModeFlexItem.displayName = 'ModeFlexItem'; export const SearchOrFilter = pure( ({ applyKqlFilterQuery, + browserFields, + dataProviders, indexPattern, - isFilterQueryDraftValid, + isRefreshPaused, + filters, + filterQuery, filterQueryDraft, + from, + fromStr, kqlMode, timelineId, + refreshInterval, + savedQueryId, + setFilters, setKqlFilterQueryDraft, + setSavedQueryId, + to, + toStr, updateKqlMode, + updateReduxTime, }) => ( @@ -90,22 +127,28 @@ export const SearchOrFilter = pure( - - - {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( - - )} - - + diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index 700489f47d0cf..a52d4ce38ccb2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -33,6 +33,7 @@ const mockUseKibanaCore = useKibanaCore as jest.Mock; jest.mock('../../lib/compose/kibana_core'); mockUseKibanaCore.mockImplementation(() => ({ uiSettings: mockUiSettings, + savedObjects: {}, })); describe('Timeline', () => { @@ -58,6 +59,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -93,6 +95,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -131,6 +134,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -169,6 +173,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -212,6 +217,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -257,6 +263,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -310,6 +317,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -367,6 +375,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -427,6 +436,7 @@ describe('Timeline', () => { id="foo" dataProviders={dataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -477,6 +487,7 @@ describe('Timeline', () => { id="foo" dataProviders={dataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -533,6 +544,7 @@ describe('Timeline', () => { id="foo" dataProviders={dataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -593,6 +605,7 @@ describe('Timeline', () => { id="foo" dataProviders={dataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index fb62b636398c2..d3a6c77db64fe 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -11,6 +11,7 @@ import * as React from 'react'; import styled from 'styled-components'; import { StaticIndexPattern } from 'ui/index_patterns'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../containers/source'; import { TimelineQuery } from '../../containers/timeline'; import { Direction } from '../../graphql/types'; @@ -61,6 +62,7 @@ interface Props { columns: ColumnHeader[]; dataProviders: DataProvider[]; end: number; + filters: esFilters.Filter[]; flyoutHeaderHeight: number; flyoutHeight: number; id: string; @@ -91,6 +93,7 @@ export const Timeline = React.memo( columns, dataProviders, end, + filters, flyoutHeaderHeight, flyoutHeight, id, @@ -119,7 +122,7 @@ export const Timeline = React.memo( dataProviders, indexPattern, browserFields, - filters: [], + filters, kqlQuery: { query: kqlQueryExpression, language: 'kuery' }, kqlMode, start, diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts index f8d615ee9a7da..eebb4a349dab4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts @@ -72,6 +72,27 @@ export const oneTimelineQuery = gql` userName favoriteDate } + filters { + meta { + alias + controlledBy + disabled + field + formattedValue + index + key + negate + params + type + value + } + query + exists + match_all + missing + range + script + } kqlMode kqlQuery { filterQuery { @@ -107,6 +128,7 @@ export const oneTimelineQuery = gql` version } title + savedQueryId sort { columnId sortDirection diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts index a170e4d53fd7b..68b749064dc0c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts @@ -60,6 +60,27 @@ export const persistTimelineMutation = gql` userName favoriteDate } + filters { + meta { + alias + controlledBy + disabled + field + formattedValue + index + key + negate + params + type + value + } + query + exists + match_all + missing + range + script + } kqlMode kqlQuery { filterQuery { @@ -75,6 +96,7 @@ export const persistTimelineMutation = gql` start end } + savedQueryId sort { columnId sortDirection diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 8732b61e5c0eb..9bde4bf47fff0 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -9044,18 +9044,6 @@ "name": "TimelineResult", "description": "", "fields": [ - { - "name": "savedObjectId", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "columns", "description": "", @@ -9072,6 +9060,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "created", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdBy", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "dataProviders", "description": "", @@ -9136,6 +9140,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filters", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "FilterTimelineResult", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "kqlMode", "description": "", @@ -9217,7 +9237,7 @@ "deprecationReason": null }, { - "name": "title", + "name": "savedQueryId", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, @@ -9225,23 +9245,27 @@ "deprecationReason": null }, { - "name": "sort", + "name": "savedObjectId", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "SortTimelineResult", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "created", + "name": "sort", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "OBJECT", "name": "SortTimelineResult", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "createdBy", + "name": "title", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, @@ -9577,6 +9601,172 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "FilterTimelineResult", + "description": "", + "fields": [ + { + "name": "exists", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "meta", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "FilterMetaTimelineResult", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "match_all", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "missing", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "query", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "range", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "script", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FilterMetaTimelineResult", + "description": "", + "fields": [ + { + "name": "alias", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "controlledBy", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "disabled", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "field", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "formattedValue", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "index", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "key", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "negate", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "params", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "SerializedFilterQueryResult", @@ -10175,6 +10365,20 @@ "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null }, + { + "name": "filters", + "description": "", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "FilterTimelineInput", "ofType": null } + } + }, + "defaultValue": null + }, { "name": "kqlMode", "description": "", @@ -10203,6 +10407,12 @@ "type": { "kind": "INPUT_OBJECT", "name": "DateRangePickerInput", "ofType": null }, "defaultValue": null }, + { + "name": "savedQueryId", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, { "name": "sort", "description": "", @@ -10401,6 +10611,136 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "FilterTimelineInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "exists", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "meta", + "description": "", + "type": { "kind": "INPUT_OBJECT", "name": "FilterMetaTimelineInput", "ofType": null }, + "defaultValue": null + }, + { + "name": "match_all", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "missing", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "query", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "range", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "script", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "FilterMetaTimelineInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "alias", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "controlledBy", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "disabled", + "description": "", + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "defaultValue": null + }, + { + "name": "field", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "formattedValue", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "index", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "key", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "negate", + "description": "", + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "defaultValue": null + }, + { + "name": "params", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "type", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "value", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "SerializedFilterQueryInput", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index a1b259b876a14..833102a0d00bc 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -122,6 +122,8 @@ export interface TimelineInput { description?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -130,6 +132,8 @@ export interface TimelineInput { dateRange?: Maybe; + savedQueryId?: Maybe; + sort?: Maybe; } @@ -185,6 +189,46 @@ export interface QueryMatchInput { operator?: Maybe; } +export interface FilterTimelineInput { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineInput { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryInput { filterQuery?: Maybe; } @@ -1760,10 +1804,12 @@ export interface SayMyName { } export interface TimelineResult { - savedObjectId: string; - columns?: Maybe; + created?: Maybe; + + createdBy?: Maybe; + dataProviders?: Maybe; dateRange?: Maybe; @@ -1774,6 +1820,8 @@ export interface TimelineResult { favorite?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -1786,13 +1834,13 @@ export interface TimelineResult { pinnedEventsSaveObject?: Maybe; - title?: Maybe; + savedQueryId?: Maybe; - sort?: Maybe; + savedObjectId: string; - created?: Maybe; + sort?: Maybe; - createdBy?: Maybe; + title?: Maybe; updated?: Maybe; @@ -1867,6 +1915,46 @@ export interface FavoriteTimelineResult { favoriteDate?: Maybe; } +export interface FilterTimelineResult { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineResult { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryResult { filterQuery?: Maybe; } @@ -4874,6 +4962,8 @@ export namespace GetOneTimeline { favorite: Maybe; + filters: Maybe; + kqlMode: Maybe; kqlQuery: Maybe; @@ -4888,6 +4978,8 @@ export namespace GetOneTimeline { title: Maybe; + savedQueryId: Maybe; + sort: Maybe; created: Maybe; @@ -5029,6 +5121,50 @@ export namespace GetOneTimeline { favoriteDate: Maybe; }; + export type Filters = { + __typename?: 'FilterTimelineResult'; + + meta: Maybe; + + query: Maybe; + + exists: Maybe; + + match_all: Maybe; + + missing: Maybe; + + range: Maybe; + + script: Maybe; + }; + + export type Meta = { + __typename?: 'FilterMetaTimelineResult'; + + alias: Maybe; + + controlledBy: Maybe; + + disabled: Maybe; + + field: Maybe; + + formattedValue: Maybe; + + index: Maybe; + + key: Maybe; + + negate: Maybe; + + params: Maybe; + + type: Maybe; + + value: Maybe; + }; + export type KqlQuery = { __typename?: 'SerializedFilterQueryResult'; @@ -5142,6 +5278,8 @@ export namespace PersistTimelineMutation { favorite: Maybe; + filters: Maybe; + kqlMode: Maybe; kqlQuery: Maybe; @@ -5150,6 +5288,8 @@ export namespace PersistTimelineMutation { dateRange: Maybe; + savedQueryId: Maybe; + sort: Maybe; created: Maybe; @@ -5257,6 +5397,50 @@ export namespace PersistTimelineMutation { favoriteDate: Maybe; }; + export type Filters = { + __typename?: 'FilterTimelineResult'; + + meta: Maybe; + + query: Maybe; + + exists: Maybe; + + match_all: Maybe; + + missing: Maybe; + + range: Maybe; + + script: Maybe; + }; + + export type Meta = { + __typename?: 'FilterMetaTimelineResult'; + + alias: Maybe; + + controlledBy: Maybe; + + disabled: Maybe; + + field: Maybe; + + formattedValue: Maybe; + + index: Maybe; + + key: Maybe; + + negate: Maybe; + + params: Maybe; + + type: Maybe; + + value: Maybe; + }; + export type KqlQuery = { __typename?: 'SerializedFilterQueryResult'; diff --git a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts index e9f4c95a80b74..81ceae68c5fb9 100644 --- a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts +++ b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { buildEsQuery, fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { buildEsQuery, fromKueryExpression, toElasticsearchQuery, JsonObject } from '@kbn/es-query'; import { isEmpty, isString, flow } from 'lodash/fp'; import { StaticIndexPattern } from 'ui/index_patterns'; import { Query } from 'src/plugins/data/common'; + import { esFilters } from '../../../../../../../src/plugins/data/public'; import { KueryFilterQuery } from '../../store'; @@ -25,6 +26,19 @@ export const convertKueryToElasticSearchQuery = ( } }; +export const convertKueryToDslFilter = ( + kueryExpression: string, + indexPattern: StaticIndexPattern +): JsonObject => { + try { + return kueryExpression + ? toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern) + : {}; + } catch (err) { + return {}; + } +}; + export const escapeQueryValue = (val: number | string = ''): string | number => { if (isString(val)) { if (isEmpty(val)) { diff --git a/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts index 9ba569cfec228..23f1f0e86dd6a 100644 --- a/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts +++ b/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts @@ -9,6 +9,7 @@ import { DEFAULT_BYTES_FORMAT, DEFAULT_KBN_VERSION, DEFAULT_TIMEZONE_BROWSER, + DEFAULT_TIMEPICKER_QUICK_RANGES, } from '../../common/constants'; export interface MockFrameworks { @@ -29,6 +30,61 @@ export const getMockKibanaUiSetting = (config: MockFrameworks) => (key: string) return ['8.0.0']; } else if (key === DEFAULT_TIMEZONE_BROWSER) { return config && config.timezone ? [config.timezone] : ['America/New_York']; + } else if (key === DEFAULT_TIMEPICKER_QUICK_RANGES) { + return [ + [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, + ], + ]; } return [null]; }; diff --git a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx index 75df1df59187f..d4a7bb1f425d4 100644 --- a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx +++ b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx @@ -18,8 +18,14 @@ import { Store } from 'redux'; import { BehaviorSubject } from 'rxjs'; import { ThemeProvider } from 'styled-components'; +import { CoreStart } from 'src/core/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; + import { createStore, State } from '../store'; import { mockGlobalState } from './global_state'; +import { mockUiSettings } from './ui_settings'; + +jest.mock('ui/new_platform'); const state: State = mockGlobalState; @@ -36,17 +42,57 @@ export const apolloClient = new ApolloClient({ export const apolloClientObservable = new BehaviorSubject(apolloClient); +const services = { + uiSettings: mockUiSettings, + savedObjects: {} as CoreStart['savedObjects'], + notifications: {} as CoreStart['notifications'], + docLinks: { + links: { + query: { + kueryQuerySyntax: '', + }, + }, + } as CoreStart['docLinks'], + http: {} as CoreStart['http'], + overlays: {} as CoreStart['overlays'], + storage: { + get: () => {}, + }, +}; + +const localStorageMock = () => { + let store: Record = {}; + + return { + getItem: (key: string) => { + return store[key] || null; + }, + setItem: (key: string, value: unknown) => { + store[key] = value; + }, + clear() { + store = {}; + }, + }; +}; + +Object.defineProperty(window, 'localStorage', { + value: localStorageMock(), +}); + /** A utility for wrapping children in the providers required to run most tests */ export const TestProviders = pure( ({ children, store = createStore(state, apolloClientObservable), onDragEnd = jest.fn() }) => ( - - - ({ eui: euiDarkVars, darkMode: true })}> - {children} - - - + + + + ({ eui: euiDarkVars, darkMode: true })}> + {children} + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts b/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts index c7803e99f3857..6c6411c6bda53 100644 --- a/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts +++ b/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts @@ -9,6 +9,7 @@ import { DEFAULT_SIEM_TIME_RANGE, DEFAULT_SIEM_REFRESH_INTERVAL, DEFAULT_INDEX_KEY, + DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ, DEFAULT_DARK_MODE, DEFAULT_TIME_RANGE, @@ -40,6 +41,8 @@ chrome.getUiSettingsClient().get.mockImplementation((key: string) => { return defaultIndexPattern; case DEFAULT_DATE_FORMAT_TZ: return 'Asia/Taipei'; + case DEFAULT_DATE_FORMAT: + return 'MMM D, YYYY @ HH:mm:ss.SSS'; case DEFAULT_DARK_MODE: return false; default: @@ -62,6 +65,9 @@ export const mockUiSettings = { get: (item: Config) => { return mockUiSettings[item]; }, + get$: () => ({ + subscribe: jest.fn(), + }), 'query:allowLeadingWildcards': true, 'query:queryString:options': {}, 'courier:ignoreFilterIfFieldNotInIndex': true, diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx index 6ceebc1708b18..f136ff72c906d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx @@ -39,10 +39,13 @@ jest.mock('../../../containers/source', () => ({ })); // Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar +// For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../components/search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../../../components/query_bar', () => ({ + QueryBar: () => null, +})); describe('body', () => { const scenariosMap = { diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx index d2c9822889c26..2d0df0b6e0033 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -38,10 +38,13 @@ jest.mock('ui/documentation_links', () => ({ })); // Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar +// For now let's forget about SiemSearchBar and QueryBar jest.mock('../../components/search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../../components/query_bar', () => ({ + QueryBar: () => null, +})); let localSource: Array<{ request: {}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx index b56b9d931af47..9e599bcfedff6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx @@ -38,10 +38,13 @@ mockUseKibanaCore.mockImplementation(() => ({ })); // Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar +// For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../components/search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../../../components/query_bar', () => ({ + QueryBar: () => null, +})); let localSource: Array<{ request: {}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx index a10118dc6ca6f..a374b0082f281 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx @@ -33,10 +33,13 @@ mockUseKibanaCore.mockImplementation(() => ({ })); // Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar +// For now let's forget about SiemSearchBar and QueryBar jest.mock('../../components/search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../../components/query_bar', () => ({ + QueryBar: () => null, +})); let localSource: Array<{ request: {}; diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts b/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts index cb2d357b74007..a2f061dc648d5 100644 --- a/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts @@ -73,3 +73,6 @@ export const globalFiltersQuerySelector = () => createSelector(selectGlobal, global => global.filters || []); export const getTimelineSelector = () => createSelector(selectTimeline, timeline => timeline); + +export const getTimelinePolicySelector = () => + createSelector(selectTimeline, timeline => timeline.policy); diff --git a/x-pack/legacy/plugins/siem/public/store/model.ts b/x-pack/legacy/plugins/siem/public/store/model.ts index 6f5bd327a1018..6f04f22866be5 100644 --- a/x-pack/legacy/plugins/siem/public/store/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/model.ts @@ -10,8 +10,10 @@ export { hostsModel } from './hosts'; export { dragAndDropModel } from './drag_and_drop'; export { networkModel } from './network'; +export type KueryFilterQueryKind = 'kuery' | 'lucene'; + export interface KueryFilterQuery { - kind: 'kuery'; + kind: KueryFilterQueryKind; expression: string; } diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts index 9729505c4e944..931d3e26172cd 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts @@ -6,6 +6,7 @@ import actionCreatorFactory from 'typescript-fsa'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header'; import { Sort } from '../../components/timeline/body/sort'; import { @@ -187,3 +188,13 @@ export const updateAutoSaveMsg = actionCreator<{ }>('UPDATE_AUTO_SAVE'); export const showCallOutUnauthorizedMsg = actionCreator('SHOW_CALL_OUT_UNAUTHORIZED_MSG'); + +export const setSavedQueryId = actionCreator<{ + id: string; + savedQueryId: string | null; +}>('SET_TIMELINE_SAVED_QUERY'); + +export const setFilters = actionCreator<{ + id: string; + filters: esFilters.Filter[]; +}>('SET_TIMELINE_FILTERS'); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts new file mode 100644 index 0000000000000..85d2e624c2807 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts @@ -0,0 +1,283 @@ +/* + * 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 { TimelineModel } from './model'; +import { Direction } from '../../graphql/types'; +import { convertTimelineAsInput } from './epic'; + +import { esFilters } from '../../../../../../../src/plugins/data/public'; +import { FilterStateStore } from '../../../../../../../src/plugins/data/common/es_query/filters'; + +describe('Epic Timeline', () => { + describe('#convertTimelineAsInput ', () => { + test('should return a TimelineInput instead of TimelineModel ', () => { + const timelineModel: TimelineModel = { + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + dataProviders: [ + { + id: 'hosts-table-hostName-DESKTOP-QBBSCUT', + name: 'DESKTOP-QBBSCUT', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'DESKTOP-QBBSCUT', + operator: ':', + }, + and: [ + { + id: + 'plain-column-renderer-data-provider-hosts-page-event_module-CQg7I24BHe9nqdOi_LYL-event_module-endgame', + name: 'event.module: endgame', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'event.module', + value: 'endgame', + operator: ':', + }, + }, + ], + }, + ], + description: '', + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + filters: [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { query: 'file' }, + type: 'phrase', + }, + query: { match_phrase: { 'event.category': 'file' } }, + }, + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: '@timestamp', + negate: false, + type: 'exists', + value: 'exists', + }, + exists: { field: '@timestamp' }, + } as esFilters.Filter, + ], + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { kind: 'kuery', expression: 'endgame.user_name : "zeus" ' }, + serializedQuery: + '{"bool":{"should":[{"match_phrase":{"endgame.user_name":"zeus"}}],"minimum_should_match":1}}', + }, + filterQueryDraft: { kind: 'kuery', expression: 'endgame.user_name : "zeus" ' }, + }, + title: 'saved', + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + dateRange: { start: 1572469587644, end: 1572555987644 }, + savedObjectId: '11169110-fc22-11e9-8ca9-072f15ce2685', + show: true, + sort: { columnId: '@timestamp', sortDirection: Direction.desc }, + width: 1100, + version: 'WzM4LDFd', + id: '11169110-fc22-11e9-8ca9-072f15ce2685', + savedQueryId: 'my endgame timeline query', + }; + + expect( + convertTimelineAsInput(timelineModel, { + kind: 'absolute', + from: 1572469587644, + fromStr: undefined, + to: 1572555987644, + toStr: undefined, + }) + ).toEqual({ + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + }, + ], + dataProviders: [ + { + and: [ + { + enabled: true, + excluded: false, + id: + 'plain-column-renderer-data-provider-hosts-page-event_module-CQg7I24BHe9nqdOi_LYL-event_module-endgame', + kqlQuery: '', + name: 'event.module: endgame', + queryMatch: { + field: 'event.module', + operator: ':', + value: 'endgame', + }, + }, + ], + enabled: true, + excluded: false, + id: 'hosts-table-hostName-DESKTOP-QBBSCUT', + kqlQuery: '', + name: 'DESKTOP-QBBSCUT', + queryMatch: { + field: 'host.name', + operator: ':', + value: 'DESKTOP-QBBSCUT', + }, + }, + ], + dateRange: { + end: 1572555987644, + start: 1572469587644, + }, + description: '', + filters: [ + { + exists: null, + match_all: null, + meta: { + alias: null, + disabled: false, + field: null, + key: 'event.category', + negate: false, + params: '{"query":"file"}', + type: 'phrase', + value: null, + }, + missing: null, + query: '{"match_phrase":{"event.category":"file"}}', + range: null, + script: null, + }, + { + exists: '{"field":"@timestamp"}', + match_all: null, + meta: { + alias: null, + disabled: false, + field: null, + key: '@timestamp', + negate: false, + params: null, + type: 'exists', + value: 'exists', + }, + missing: null, + query: null, + range: null, + script: null, + }, + ], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + expression: 'endgame.user_name : "zeus" ', + kind: 'kuery', + }, + serializedQuery: + '{"bool":{"should":[{"match_phrase":{"endgame.user_name":"zeus"}}],"minimum_should_match":1}}', + }, + }, + savedQueryId: 'my endgame timeline query', + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + title: 'saved', + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts index 6957db5578af5..a9cf7cff812ad 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, has, merge as mergeObject, set, omit } from 'lodash/fp'; +import { + get, + has, + merge as mergeObject, + set, + omit, + isObject, + toString as fpToString, +} from 'lodash/fp'; import { Action } from 'redux'; import { Epic } from 'redux-observable'; import { from, Observable, empty, merge } from 'rxjs'; @@ -20,6 +28,7 @@ import { takeUntil, } from 'rxjs/operators'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header'; import { persistTimelineMutation } from '../../containers/timeline/persist.gql_query'; import { @@ -52,6 +61,8 @@ import { updateTimeline, updateTitle, updateAutoSaveMsg, + setFilters, + setSavedQueryId, startTimelineSaving, endTimelineSaving, createTimeline, @@ -81,6 +92,8 @@ const timelineActionsType = [ dataProviderEdited.type, removeColumn.type, removeProvider.type, + setFilters.type, + setSavedQueryId.type, updateColumns.type, updateDataProviderEnabled.type, updateDataProviderExcluded.type, @@ -235,14 +248,16 @@ const timelineInput: TimelineInput = { columns: null, dataProviders: null, description: null, + filters: null, kqlMode: null, kqlQuery: null, title: null, dateRange: null, + savedQueryId: null, sort: null, }; -const convertTimelineAsInput = ( +export const convertTimelineAsInput = ( timeline: TimelineModel, timelineTimeRange: TimeRange ): TimelineInput => @@ -258,6 +273,65 @@ const convertTimelineAsInput = ( get(key, timeline).map((col: ColumnHeader) => omit(['width', '__typename'], col)), acc ); + } else if (key === 'filters' && get(key, timeline) != null) { + const filters = get(key, timeline); + return set( + key, + filters != null + ? filters.map((myFilter: esFilters.Filter) => { + const basicFilter = omit(['$state'], myFilter); + return { + ...basicFilter, + meta: { + ...basicFilter.meta, + field: + (esFilters.isMatchAllFilter(basicFilter) || + esFilters.isPhraseFilter(basicFilter) || + esFilters.isPhrasesFilter(basicFilter) || + esFilters.isRangeFilter(basicFilter)) && + basicFilter.meta.field != null + ? convertToString(basicFilter.meta.field) + : null, + value: + basicFilter.meta.value != null + ? convertToString(basicFilter.meta.value) + : null, + params: + basicFilter.meta.params != null + ? convertToString(basicFilter.meta.params) + : null, + }, + ...(esFilters.isMatchAllFilter(basicFilter) + ? { + match_all: convertToString( + (basicFilter as esFilters.MatchAllFilter).match_all + ), + } + : { match_all: null }), + ...(esFilters.isMissingFilter(basicFilter) && basicFilter.missing != null + ? { missing: convertToString(basicFilter.missing) } + : { missing: null }), + ...(esFilters.isExistsFilter(basicFilter) && basicFilter.exists != null + ? { exists: convertToString(basicFilter.exists) } + : { exists: null }), + ...((esFilters.isQueryStringFilter(basicFilter) || + get('query', basicFilter) != null) && + basicFilter.query != null + ? { query: convertToString(basicFilter.query) } + : { query: null }), + ...(esFilters.isRangeFilter(basicFilter) && basicFilter.range != null + ? { range: convertToString(basicFilter.range) } + : { range: null }), + ...(esFilters.isRangeFilter(basicFilter) && + basicFilter.script != + null /* TODO remove it when PR50713 is merged || esFilters.isPhraseFilter(basicFilter) */ + ? { script: convertToString(basicFilter.script) } + : { script: null }), + }; + }) + : [], + acc + ); } return set(key, get(key, timeline), acc); } @@ -271,3 +345,14 @@ const omitTypenameInTimeline = ( oldTimeline: TimelineModel, newTimeline: TimelineResult ): TimelineModel => JSON.parse(JSON.stringify(mergeObject(oldTimeline, newTimeline)), omitTypename); + +const convertToString = (obj: unknown) => { + try { + if (isObject(obj)) { + return JSON.stringify(obj); + } + return fpToString(obj); + } catch { + return ''; + } +}; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts index eee467cd9d6d4..16ae53ade7969 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts @@ -3,8 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { getOr, omit, uniq, isEmpty, isEqualWith } from 'lodash/fp'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header'; import { getColumnWidthFromType } from '../../components/timeline/body/helpers'; import { Sort } from '../../components/timeline/body/sort'; @@ -1135,3 +1137,43 @@ export const updateHighlightedDropAndProvider = ({ }, }; }; + +interface UpdateSavedQueryParams { + id: string; + savedQueryId: string | null; + timelineById: TimelineById; +} + +export const updateSavedQuery = ({ + id, + savedQueryId, + timelineById, +}: UpdateSavedQueryParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + savedQueryId, + }, + }; +}; + +interface UpdateFiltersParams { + id: string; + filters: esFilters.Filter[]; + timelineById: TimelineById; +} + +export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + filters, + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts index 3b10314f72531..405564a4b5b0d 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header'; import { DataProvider } from '../../components/timeline/data_providers/data_provider'; import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/helpers'; @@ -25,6 +26,7 @@ export interface TimelineModel { description: string; /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ eventIdToNoteIds: Record; + filters?: esFilters.Filter[]; /** The chronological history of actions related to this timeline */ historyIds: string[]; /** The chronological history of actions related to this timeline */ @@ -59,6 +61,7 @@ export interface TimelineModel { start: number; end: number; }; + savedQueryId?: string | null; /** When true, show the timeline flyover */ show: boolean; /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ @@ -77,6 +80,7 @@ export const timelineDefaults: Readonly ({ ...state, - timelineById: applyKqlFilterQueryDraft({ id, filterQuery, timelineById: state.timelineById }), + timelineById: applyKqlFilterQueryDraft({ + id, + filterQuery, + timelineById: state.timelineById, + }), })) .case(setKqlFilterQueryDraft, (state, { id, filterQueryDraft }) => ({ ...state, @@ -361,4 +369,20 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, showCallOutUnauthorizedMsg: true, })) + .case(setSavedQueryId, (state, { id, savedQueryId }) => ({ + ...state, + timelineById: updateSavedQuery({ + id, + savedQueryId, + timelineById: state.timelineById, + }), + })) + .case(setFilters, (state, { id, filters }) => ({ + ...state, + timelineById: updateFilters({ + id, + filters, + timelineById: state.timelineById, + }), + })) .build(); diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx index de6fedb687d97..b70a5432e47f8 100644 --- a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx @@ -14,12 +14,6 @@ mockDispatch.mockImplementation(fn => fn); const applyTimelineKqlMock: jest.Mock = (dispatchApplyTimelineFilterQuery as unknown) as jest.Mock; -jest.mock('../../store/hosts/actions', () => ({ - applyHostsFilterQuery: jest.fn(), -})); -jest.mock('../../store/network/actions', () => ({ - applyNetworkFilterQuery: jest.fn(), -})); jest.mock('../../store/timeline/actions', () => ({ applyKqlFilterQuery: jest.fn(), })); @@ -36,7 +30,6 @@ describe('#useUpdateKql', () => { kueryFilterQuery: { expression: '', kind: 'kuery' }, kueryFilterQueryDraft: { expression: 'host.name: "myLove"', kind: 'kuery' }, storeType: 'timelineType', - type: null, timelineId: 'myTimelineId', })(mockDispatch); expect(applyTimelineKqlMock).toHaveBeenCalledWith({ diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx index 1a4ca656ba0fe..b5843d149d24d 100644 --- a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx @@ -18,7 +18,6 @@ interface UseUpdateKqlProps { kueryFilterQuery: KueryFilterQuery | null; kueryFilterQueryDraft: KueryFilterQuery | null; storeType: 'timelineType'; - type: null; timelineId?: string; } @@ -28,7 +27,6 @@ export const useUpdateKql = ({ kueryFilterQueryDraft, storeType, timelineId, - type, }: UseUpdateKqlProps): RefetchKql => { const updateKql: RefetchKql = (dispatch: Dispatch) => { if (kueryFilterQueryDraft != null && !isEqual(kueryFilterQuery, kueryFilterQueryDraft)) { @@ -37,10 +35,7 @@ export const useUpdateKql = ({ dispatchApplyTimelineFilterQuery({ id: timelineId, filterQuery: { - kuery: { - kind: 'kuery', - expression: kueryFilterQueryDraft.expression, - }, + kuery: kueryFilterQueryDraft, serializedQuery: convertKueryToElasticSearchQuery( kueryFilterQueryDraft.expression, indexPattern @@ -49,7 +44,6 @@ export const useUpdateKql = ({ }) ); } - return true; } return false; diff --git a/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx b/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx new file mode 100644 index 0000000000000..f1e4cf3411398 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect } from 'react'; +import { + SavedQueryService, + createSavedQueryService, +} from '../../../../../../../src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service'; + +import { useKibanaCore } from '../../lib/compose/kibana_core'; + +export const useSavedQueryServices = () => { + const core = useKibanaCore(); + const [savedQueryService, setSavedQueryService] = useState( + createSavedQueryService(core.savedObjects.client) + ); + + useEffect(() => { + setSavedQueryService(createSavedQueryService(core.savedObjects.client)); + }, [core.savedObjects.client]); + return savedQueryService; +}; diff --git a/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts index a417ecb82b2d2..f05c26de7f75c 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts @@ -49,6 +49,20 @@ const sortTimeline = ` sortDirection: String `; +const filtersMetaTimeline = ` + alias: String + controlledBy: String + disabled: Boolean + field: String + formattedValue: String + index: String + key: String + negate: Boolean + params: String + type: String + value: String +`; + export const timelineSchema = gql` ############### #### INPUT #### @@ -97,14 +111,30 @@ export const timelineSchema = gql` ${sortTimeline} } + input FilterMetaTimelineInput { + ${filtersMetaTimeline} + } + + input FilterTimelineInput { + exists: String + meta: FilterMetaTimelineInput + match_all: String + missing: String + query: String + range: String + script: String + } + input TimelineInput { columns: [ColumnHeaderInput!] dataProviders: [DataProviderInput!] description: String + filters: [FilterTimelineInput!] kqlMode: String kqlQuery: SerializedFilterQueryInput title: String dateRange: DateRangePickerInput + savedQueryId: String sort: SortTimelineInput } @@ -171,24 +201,40 @@ export const timelineSchema = gql` ${sortTimeline} } + type FilterMetaTimelineResult { + ${filtersMetaTimeline} + } + + type FilterTimelineResult { + exists: String + meta: FilterMetaTimelineResult + match_all: String + missing: String + query: String + range: String + script: String + } + type TimelineResult { - savedObjectId: String! columns: [ColumnHeaderResult!] + created: Float + createdBy: String dataProviders: [DataProviderResult!] dateRange: DateRangePickerResult description: String eventIdToNoteIds: [NoteResult!] favorite: [FavoriteTimelineResult!] + filters: [FilterTimelineResult!] kqlMode: String kqlQuery: SerializedFilterQueryResult notes: [NoteResult!] noteIds: [String!] pinnedEventIds: [String!] pinnedEventsSaveObject: [PinnedEvent!] - title: String + savedQueryId: String + savedObjectId: String! sort: SortTimelineResult - created: Float - createdBy: String + title: String updated: Float updatedBy: String version: String! diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index cf7ce3ad02fa1..d6a4d204124a1 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -124,6 +124,8 @@ export interface TimelineInput { description?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -132,6 +134,8 @@ export interface TimelineInput { dateRange?: Maybe; + savedQueryId?: Maybe; + sort?: Maybe; } @@ -187,6 +191,46 @@ export interface QueryMatchInput { operator?: Maybe; } +export interface FilterTimelineInput { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineInput { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryInput { filterQuery?: Maybe; } @@ -1762,10 +1806,12 @@ export interface SayMyName { } export interface TimelineResult { - savedObjectId: string; - columns?: Maybe; + created?: Maybe; + + createdBy?: Maybe; + dataProviders?: Maybe; dateRange?: Maybe; @@ -1776,6 +1822,8 @@ export interface TimelineResult { favorite?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -1788,13 +1836,13 @@ export interface TimelineResult { pinnedEventsSaveObject?: Maybe; - title?: Maybe; + savedQueryId?: Maybe; - sort?: Maybe; + savedObjectId: string; - created?: Maybe; + sort?: Maybe; - createdBy?: Maybe; + title?: Maybe; updated?: Maybe; @@ -1869,6 +1917,46 @@ export interface FavoriteTimelineResult { favoriteDate?: Maybe; } +export interface FilterTimelineResult { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineResult { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryResult { filterQuery?: Maybe; } @@ -7469,10 +7557,12 @@ export namespace SayMyNameResolvers { export namespace TimelineResultResolvers { export interface Resolvers { - savedObjectId?: SavedObjectIdResolver; - columns?: ColumnsResolver, TypeParent, TContext>; + created?: CreatedResolver, TypeParent, TContext>; + + createdBy?: CreatedByResolver, TypeParent, TContext>; + dataProviders?: DataProvidersResolver, TypeParent, TContext>; dateRange?: DateRangeResolver, TypeParent, TContext>; @@ -7483,6 +7573,8 @@ export namespace TimelineResultResolvers { favorite?: FavoriteResolver, TypeParent, TContext>; + filters?: FiltersResolver, TypeParent, TContext>; + kqlMode?: KqlModeResolver, TypeParent, TContext>; kqlQuery?: KqlQueryResolver, TypeParent, TContext>; @@ -7499,13 +7591,13 @@ export namespace TimelineResultResolvers { TContext >; - title?: TitleResolver, TypeParent, TContext>; + savedQueryId?: SavedQueryIdResolver, TypeParent, TContext>; - sort?: SortResolver, TypeParent, TContext>; + savedObjectId?: SavedObjectIdResolver; - created?: CreatedResolver, TypeParent, TContext>; + sort?: SortResolver, TypeParent, TContext>; - createdBy?: CreatedByResolver, TypeParent, TContext>; + title?: TitleResolver, TypeParent, TContext>; updated?: UpdatedResolver, TypeParent, TContext>; @@ -7514,13 +7606,18 @@ export namespace TimelineResultResolvers { version?: VersionResolver; } - export type SavedObjectIdResolver< - R = string, + export type ColumnsResolver< + R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type ColumnsResolver< - R = Maybe, + export type CreatedResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; + export type CreatedByResolver< + R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; @@ -7549,6 +7646,11 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver; + export type FiltersResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; export type KqlModeResolver< R = Maybe, Parent = TimelineResult, @@ -7579,22 +7681,22 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type TitleResolver< + export type SavedQueryIdResolver< R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type SortResolver< - R = Maybe, + export type SavedObjectIdResolver< + R = string, Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type CreatedResolver< - R = Maybe, + export type SortResolver< + R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type CreatedByResolver< + export type TitleResolver< R = Maybe, Parent = TimelineResult, TContext = SiemContext @@ -7837,6 +7939,142 @@ export namespace FavoriteTimelineResultResolvers { > = Resolver; } +export namespace FilterTimelineResultResolvers { + export interface Resolvers { + exists?: ExistsResolver, TypeParent, TContext>; + + meta?: MetaResolver, TypeParent, TContext>; + + match_all?: MatchAllResolver, TypeParent, TContext>; + + missing?: MissingResolver, TypeParent, TContext>; + + query?: QueryResolver, TypeParent, TContext>; + + range?: RangeResolver, TypeParent, TContext>; + + script?: ScriptResolver, TypeParent, TContext>; + } + + export type ExistsResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type MetaResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type MatchAllResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type MissingResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type QueryResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type RangeResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type ScriptResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; +} + +export namespace FilterMetaTimelineResultResolvers { + export interface Resolvers { + alias?: AliasResolver, TypeParent, TContext>; + + controlledBy?: ControlledByResolver, TypeParent, TContext>; + + disabled?: DisabledResolver, TypeParent, TContext>; + + field?: FieldResolver, TypeParent, TContext>; + + formattedValue?: FormattedValueResolver, TypeParent, TContext>; + + index?: IndexResolver, TypeParent, TContext>; + + key?: KeyResolver, TypeParent, TContext>; + + negate?: NegateResolver, TypeParent, TContext>; + + params?: ParamsResolver, TypeParent, TContext>; + + type?: TypeResolver, TypeParent, TContext>; + + value?: ValueResolver, TypeParent, TContext>; + } + + export type AliasResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type ControlledByResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type DisabledResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type FieldResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type FormattedValueResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type IndexResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type KeyResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type NegateResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type ParamsResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type TypeResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type ValueResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; +} + export namespace SerializedFilterQueryResultResolvers { export interface Resolvers { filterQuery?: FilterQueryResolver, TypeParent, TContext>; @@ -8484,6 +8722,8 @@ export type IResolvers = { QueryMatchResult?: QueryMatchResultResolvers.Resolvers; DateRangePickerResult?: DateRangePickerResultResolvers.Resolvers; FavoriteTimelineResult?: FavoriteTimelineResultResolvers.Resolvers; + FilterTimelineResult?: FilterTimelineResultResolvers.Resolvers; + FilterMetaTimelineResult?: FilterMetaTimelineResultResolvers.Resolvers; SerializedFilterQueryResult?: SerializedFilterQueryResultResolvers.Resolvers; SerializedKueryQueryResult?: SerializedKueryQueryResultResolvers.Resolvers; KueryFilterQueryResult?: KueryFilterQueryResultResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts index f9175eb8ffb90..8c7275a86911b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts @@ -146,6 +146,65 @@ export const timelineSavedObjectMappings: { }, }, }, + filters: { + properties: { + meta: { + properties: { + alias: { + type: 'text', + }, + controlledBy: { + type: 'text', + }, + disabled: { + type: 'boolean', + }, + field: { + type: 'text', + }, + formattedValue: { + type: 'text', + }, + index: { + type: 'keyword', + }, + key: { + type: 'keyword', + }, + negate: { + type: 'boolean', + }, + params: { + type: 'text', + }, + type: { + type: 'keyword', + }, + value: { + type: 'text', + }, + }, + }, + exists: { + type: 'text', + }, + match_all: { + type: 'text', + }, + missing: { + type: 'text', + }, + query: { + type: 'text', + }, + range: { + type: 'text', + }, + script: { + type: 'text', + }, + }, + }, kqlMode: { type: 'keyword', }, @@ -183,6 +242,9 @@ export const timelineSavedObjectMappings: { }, }, }, + savedQueryId: { + type: 'keyword', + }, sort: { properties: { columnId: { diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts index 0ffdf73f4c74f..72e5cd50af394 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts @@ -59,6 +59,33 @@ const SavedDataProviderRuntimeType = runtimeTypes.partial({ and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)), }); +/* + * Filters Types + */ +const SavedFilterMetaRuntimeType = runtimeTypes.partial({ + alias: unionWithNullType(runtimeTypes.string), + controlledBy: unionWithNullType(runtimeTypes.string), + disabled: unionWithNullType(runtimeTypes.boolean), + field: unionWithNullType(runtimeTypes.string), + formattedValue: unionWithNullType(runtimeTypes.string), + index: unionWithNullType(runtimeTypes.string), + key: unionWithNullType(runtimeTypes.string), + negate: unionWithNullType(runtimeTypes.boolean), + params: unionWithNullType(runtimeTypes.string), + type: unionWithNullType(runtimeTypes.string), + value: unionWithNullType(runtimeTypes.string), +}); + +const SavedFilterRuntimeType = runtimeTypes.partial({ + exists: unionWithNullType(runtimeTypes.string), + meta: unionWithNullType(SavedFilterMetaRuntimeType), + match_all: unionWithNullType(runtimeTypes.string), + missing: unionWithNullType(runtimeTypes.string), + query: unionWithNullType(runtimeTypes.string), + range: unionWithNullType(runtimeTypes.string), + script: unionWithNullType(runtimeTypes.string), +}); + /* * kqlQuery -> filterQuery Types */ @@ -110,10 +137,12 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({ dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), description: unionWithNullType(runtimeTypes.string), favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), + filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), kqlMode: unionWithNullType(runtimeTypes.string), kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType), title: unionWithNullType(runtimeTypes.string), dateRange: unionWithNullType(SavedDateRangePickerRuntimeType), + savedQueryId: unionWithNullType(runtimeTypes.string), sort: unionWithNullType(SavedSortRuntimeType), created: unionWithNullType(runtimeTypes.number), createdBy: unionWithNullType(runtimeTypes.string), From eb0141d6609185dfcb43750d13d9b8bb51f5e5a7 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Fri, 15 Nov 2019 09:06:22 +0100 Subject: [PATCH 05/44] [SIEM] Fix IE11 timeline drag and drop issue (#50528) --- .../page/add_filter_to_global_search_bar/index.tsx | 6 +++--- .../components/timeline/data_providers/providers.tsx | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx index 6e1e6545e5534..012d8322fa329 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx @@ -33,7 +33,7 @@ export const AddFilterToGlobalSearchBar = React.memo( return ( + @@ -51,11 +51,11 @@ export const HoverActionsContainer = styled(EuiPanel)` align-items: center; display: flex; flex-direction: row; - height: 25px; + height: 34px; justify-content: center; left: 5px; position: absolute; top: -10px; - width: 30px; + width: 34px; cursor: pointer; `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx index ac0f9bbe9c878..112962367cd36 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx @@ -61,6 +61,14 @@ PanelProviders.displayName = 'PanelProviders'; const PanelProvidersGroupContainer = styled(EuiFlexGroup)` position: relative; flex-grow: unset; + + .euiFlexItem { + flex: 1 0 auto; + } + + .euiFlexItem--flexGrowZero { + flex: 0 0 auto; + } `; PanelProvidersGroupContainer.displayName = 'PanelProvidersGroupContainer'; @@ -68,6 +76,7 @@ PanelProvidersGroupContainer.displayName = 'PanelProvidersGroupContainer'; /** A row of data providers in the timeline drop zone */ const PanelProviderGroupContainer = styled(EuiFlexGroup)` height: ${ROW_OF_DATA_PROVIDERS_HEIGHT}px; + min-height: ${ROW_OF_DATA_PROVIDERS_HEIGHT}px; margin: 5px 0px; `; From 38899f0879515bf96d9dbfe18cdc288e880fc27a Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Fri, 15 Nov 2019 09:57:52 +0100 Subject: [PATCH 06/44] fixes conditional links tests (#50642) --- .../ml_conditional_links.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts index 5485942c0f624..a03ff0c1845f8 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts @@ -104,7 +104,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', - '/app/siem#/network/ip/127.0.0.1?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/siem#/network/ip/127.0.0.1?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -112,7 +112,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', - "/app/siem#/network/ip/127.0.0.1?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + "/app/siem#/network/ip/127.0.0.1?_g=()&query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" ); }); @@ -120,7 +120,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkMultipleIpNullKqlQuery); cy.url().should( 'include', - "/app/siem#/network?query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + "app/siem#/network/flows?_g=()&query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999))" ); }); @@ -128,7 +128,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkMultipleIpKqlQuery); cy.url().should( 'include', - "/app/siem#/network?query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + "/app/siem#/network/flows?_g=()&query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" ); }); @@ -136,7 +136,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkNullKqlQuery); cy.url().should( 'include', - '/app/siem#/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/siem#/network/flows?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -144,7 +144,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkKqlQuery); cy.url().should( 'include', - "/app/siem#/network?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + "/app/siem#/network/flows?_g=()&query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" ); }); From c3d0349fb04717282c5e5452fbc7404f54d50596 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 15 Nov 2019 10:29:38 +0100 Subject: [PATCH 07/44] fix: hide 'edit' button for mobile for dashboards (#50639) --- .../navigation/public/top_nav_menu/top_nav_menu_item.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index d4d1c159906aa..4d3b72bae6411 100644 --- a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -45,6 +45,7 @@ export function TopNavMenuItem(props: TopNavMenuData) { isDisabled={isDisabled()} onClick={handleClick} data-test-subj={props.testId} + className={props.className} > {capitalize(props.label || props.id!)} From 70d6220a41d96f25b0634f3962de6a1fa340eefb Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Fri, 15 Nov 2019 10:33:29 +0100 Subject: [PATCH 08/44] Upgrade to TypeScript 3.7.2 (#47188) * Update TS to 3.7.0-beta * Upgrade to TypeScript 3.7.2 * Upgrade cypress to 3.5.0 * Upgrade apollo-link-http * Update prettier * Fix expression types * Fix Lens test typings * Fix ML breakage * Fix APM breakage * Fix sinon stub typing * Fix beats management types * Fix WMSOptions types * Fix ui_filters in APM * APM: Explicitly type Setup to prevent hitting TS limits * Change file name to correct case * Add styleguide rule for optional chaining * Update typescript-eslint packages * Revert changes in ui filters routes that are no longer needed --- STYLEGUIDE.md | 1 + package.json | 8 +- packages/eslint-config-kibana/package.json | 4 +- packages/kbn-analytics/package.json | 2 +- packages/kbn-config-schema/package.json | 2 +- packages/kbn-dev-utils/package.json | 2 +- packages/kbn-elastic-idx/package.json | 2 +- packages/kbn-i18n/package.json | 2 +- packages/kbn-pm/package.json | 2 +- .../console/server/request.test.ts | 4 +- .../public/components/wms_options.tsx | 19 +-- src/plugins/expressions/public/types/index.ts | 2 - .../plugins/core_plugin_a/package.json | 2 +- .../plugins/core_plugin_b/package.json | 2 +- .../plugins/core_plugin_legacy/package.json | 2 +- .../kbn_tp_embeddable_explorer/package.json | 2 +- .../kbn_tp_sample_panel_action/package.json | 2 +- .../legacy/plugins/apm/cypress/package.json | 4 +- .../apm/server/lib/helpers/setup_request.ts | 24 +++- .../lib/metrics/transform_metrics_chart.ts | 7 +- .../public/utils/typed_react.ts | 4 +- .../indexpattern_plugin/datapanel.test.tsx | 4 +- .../data_frame_analytics/common/analytics.ts | 1 - .../csv/server/lib/__tests__/hit_iterator.ts | 2 +- x-pack/package.json | 6 +- x-pack/typings/global_fetch.d.ts | 9 ++ yarn.lock | 116 +++++++++++------- 27 files changed, 145 insertions(+), 92 deletions(-) create mode 100644 x-pack/typings/global_fetch.d.ts diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 5fd3ef5e8ff4b..461d51a3e76e3 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -120,6 +120,7 @@ You should prefer modern language features in a lot of cases, e.g.: * Prefer arrow function over storing `this` (no `const self = this;`) * Prefer template strings over string concatenation * Prefer the spread operator for copying arrays (`[...arr]`) over `arr.slice()` +* Use optional chaining (`?.`) and nullish Coalescing (`??`) over `lodash.get` (and similar utilities) ### Avoid mutability and state diff --git a/package.json b/package.json index 8fa9bf1847eb8..c0e2acdb578c4 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "**/@types/react": "16.8.3", "**/@types/hapi": "^17.0.18", "**/@types/angular": "^1.6.56", - "**/typescript": "3.5.3", + "**/typescript": "3.7.2", "**/graphql-toolkit/lodash": "^4.17.13", "**/isomorphic-git/**/base64-js": "^1.2.1", "**/image-diff/gm/debug": "^2.6.9" @@ -346,8 +346,8 @@ "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^2.5.0", - "@typescript-eslint/parser": "^2.5.0", + "@typescript-eslint/eslint-plugin": "^2.7.0", + "@typescript-eslint/parser": "^2.7.0", "angular-mocks": "^1.7.8", "archiver": "^3.1.1", "axe-core": "^3.3.2", @@ -444,7 +444,7 @@ "supertest": "^3.1.0", "supertest-as-promised": "^4.0.2", "tree-kill": "^1.2.1", - "typescript": "3.5.3", + "typescript": "3.7.2", "typings-tester": "^0.3.2", "vinyl-fs": "^3.0.3", "xml2js": "^0.4.22", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index b5079a49c8385..c67629f058d5a 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -15,8 +15,8 @@ }, "homepage": "https://github.com/elastic/eslint-config-kibana#readme", "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^2.5.0", - "@typescript-eslint/parser": "^2.5.0", + "@typescript-eslint/eslint-plugin": "^2.7.0", + "@typescript-eslint/parser": "^2.7.0", "babel-eslint": "^10.0.3", "eslint": "^6.5.1", "eslint-plugin-babel": "^5.3.0", diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json index b0ac86b465a62..f59fbf4720835 100644 --- a/packages/kbn-analytics/package.json +++ b/packages/kbn-analytics/package.json @@ -17,6 +17,6 @@ "@babel/cli": "7.5.5", "@kbn/dev-utils": "1.0.0", "@kbn/babel-preset": "1.0.0", - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/packages/kbn-config-schema/package.json b/packages/kbn-config-schema/package.json index 4880fb4ebfdee..71c0ae4bff1f9 100644 --- a/packages/kbn-config-schema/package.json +++ b/packages/kbn-config-schema/package.json @@ -10,7 +10,7 @@ "kbn:bootstrap": "yarn build" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" }, "peerDependencies": { "joi": "^13.5.2", diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index e8781f6d901d9..09753afeb120f 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -21,7 +21,7 @@ "tslib": "^1.9.3" }, "devDependencies": { - "typescript": "3.5.3", + "typescript": "3.7.2", "@kbn/expect": "1.0.0", "chance": "1.0.18" } diff --git a/packages/kbn-elastic-idx/package.json b/packages/kbn-elastic-idx/package.json index abfaea75357dd..9532983942d6b 100644 --- a/packages/kbn-elastic-idx/package.json +++ b/packages/kbn-elastic-idx/package.json @@ -20,7 +20,7 @@ "@babel/core": "^7.5.5", "@babel/plugin-transform-async-to-generator": "^7.5.0", "jest": "^24.9.0", - "typescript": "3.5.3" + "typescript": "3.7.2" }, "jest": { "testEnvironment": "node" diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 8a88626bffbe8..3e25ceb8714df 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -21,7 +21,7 @@ "del": "^5.1.0", "getopts": "^2.2.4", "supports-color": "^7.0.0", - "typescript": "3.5.3" + "typescript": "3.7.2" }, "dependencies": { "intl-format-cache": "^2.1.0", diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index ac46dd02757cf..2f9b177be6532 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -58,7 +58,7 @@ "strip-ansi": "^4.0.0", "strong-log-transformer": "^2.1.0", "tempy": "^0.3.0", - "typescript": "3.5.3", + "typescript": "3.7.2", "unlazy-loader": "^0.1.3", "webpack": "^4.41.0", "webpack-cli": "^3.3.9", diff --git a/src/legacy/core_plugins/console/server/request.test.ts b/src/legacy/core_plugins/console/server/request.test.ts index 463649a090295..d5504c0f3a3c2 100644 --- a/src/legacy/core_plugins/console/server/request.test.ts +++ b/src/legacy/core_plugins/console/server/request.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import http from 'http'; +import http, { ClientRequest } from 'http'; import * as sinon from 'sinon'; import { sendRequest } from './request'; import { URL } from 'url'; @@ -24,7 +24,7 @@ import { fail } from 'assert'; describe(`Console's send request`, () => { let sandbox: sinon.SinonSandbox; - let stub: sinon.SinonStub; + let stub: sinon.SinonStub, ClientRequest>; let fakeRequest: http.ClientRequest; beforeEach(() => { diff --git a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx b/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx index ef6b2eaea1e52..c5ccc3acba610 100644 --- a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx @@ -23,23 +23,26 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { TmsLayer } from 'ui/vis/map/service_settings'; +import { Vis } from 'ui/vis'; +import { RegionMapVisParams } from '../../../region_map/public/types'; import { SelectOption, SwitchOption } from '../../../kbn_vislib_vis_types/public/components'; -import { RegionMapOptionsProps } from '../../../region_map/public/components/region_map_options'; import { WmsInternalOptions } from './wms_internal_options'; -import { TileMapOptionsProps } from './tile_map_options'; -import { TileMapVisParams } from '../types'; +import { WMSOptions, TileMapVisParams } from '../types'; + +interface Props { + stateParams: TileMapVisParams | RegionMapVisParams; + setValue: (title: 'wms', options: WMSOptions) => void; + vis: Vis; +} const mapLayerForOption = ({ id }: TmsLayer) => ({ text: id, value: id }); -function WmsOptions({ stateParams, setValue, vis }: TileMapOptionsProps | RegionMapOptionsProps) { +function WmsOptions({ stateParams, setValue, vis }: Props) { const { wms } = stateParams; const { tmsLayers } = vis.type.editorConfig.collections; const tmsLayerOptions = useMemo(() => tmsLayers.map(mapLayerForOption), [tmsLayers]); - const setWmsOption = ( - paramName: T, - value: TileMapVisParams['wms'][T] - ) => + const setWmsOption = (paramName: T, value: WMSOptions[T]) => setValue('wms', { ...wms, [paramName]: value, diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 87ef810682f60..faceef4f90a6f 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -21,8 +21,6 @@ import { ExpressionInterpret } from '../interpreter_provider'; import { TimeRange } from '../../../data/public'; import { Adapters } from '../../../inspector/public'; import { Query } from '../../../data/public'; -import { ExpressionAST } from '../../../expressions/public'; -import { ExpressionArgAST } from '../../../../plugins/expressions/public'; import { esFilters } from '../../../../plugins/data/public'; export { ArgumentType } from './arguments'; diff --git a/test/plugin_functional/plugins/core_plugin_a/package.json b/test/plugin_functional/plugins/core_plugin_a/package.json index 7dede34f3c0cf..060ae49f43e8a 100644 --- a/test/plugin_functional/plugins/core_plugin_a/package.json +++ b/test/plugin_functional/plugins/core_plugin_a/package.json @@ -12,6 +12,6 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/core_plugin_b/package.json b/test/plugin_functional/plugins/core_plugin_b/package.json index 18ff8cf7cc5c9..3eb878b9ed5dc 100644 --- a/test/plugin_functional/plugins/core_plugin_b/package.json +++ b/test/plugin_functional/plugins/core_plugin_b/package.json @@ -12,6 +12,6 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/core_plugin_legacy/package.json b/test/plugin_functional/plugins/core_plugin_legacy/package.json index 2ae83b28f7e85..5f784c7b836a5 100644 --- a/test/plugin_functional/plugins/core_plugin_legacy/package.json +++ b/test/plugin_functional/plugins/core_plugin_legacy/package.json @@ -12,6 +12,6 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 196e64af39985..9df9352f76fc2 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -17,6 +17,6 @@ }, "devDependencies": { "@kbn/plugin-helpers": "9.0.2", - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index 33e60128d0806..054276b620907 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -17,6 +17,6 @@ }, "devDependencies": { "@kbn/plugin-helpers": "9.0.2", - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/x-pack/legacy/plugins/apm/cypress/package.json b/x-pack/legacy/plugins/apm/cypress/package.json index 98dcd495b8594..ef8955fcbd1b0 100644 --- a/x-pack/legacy/plugins/apm/cypress/package.json +++ b/x-pack/legacy/plugins/apm/cypress/package.json @@ -11,11 +11,11 @@ "@cypress/snapshot": "^2.1.3", "@cypress/webpack-preprocessor": "^4.1.0", "@types/js-yaml": "^3.12.1", - "cypress": "^3.4.1", + "cypress": "^3.5.0", "js-yaml": "^3.13.1", "p-limit": "^2.2.1", "ts-loader": "^6.1.0", - "typescript": "^3.6.3", + "typescript": "3.7.2", "webpack": "^4.40.2" } } diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index dcc034287863a..ab0f47eb04d62 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -7,10 +7,15 @@ import { Legacy } from 'kibana'; import { Server } from 'hapi'; import moment from 'moment'; +import { KibanaConfig } from 'src/legacy/server/kbn_server'; import { getESClient } from './es_client'; import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; -import { PromiseReturnType } from '../../../typings/common'; -import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; +import { + getApmIndices, + ApmIndicesConfig +} from '../settings/apm_indices/get_apm_indices'; +import { ESFilter } from '../../../typings/elasticsearch'; +import { ESClient } from './es_client'; function decodeUiFilters(server: Server, uiFiltersEncoded?: string) { if (!uiFiltersEncoded) { @@ -26,9 +31,20 @@ export interface APMRequestQuery { end?: string; uiFilters?: string; } +// Explicitly type Setup to prevent TS initialization errors +// https://github.com/microsoft/TypeScript/issues/34933 -export type Setup = PromiseReturnType; -export async function setupRequest(req: Legacy.Request) { +export interface Setup { + start: number; + end: number; + uiFiltersES: ESFilter[]; + client: ESClient; + internalClient: ESClient; + config: KibanaConfig; + indices: ApmIndicesConfig; +} + +export async function setupRequest(req: Legacy.Request): Promise { const query = (req.query as unknown) as APMRequestQuery; const { server } = req; const savedObjectsClient = server.savedObjects.getScopedSavedObjectsClient( diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index 594a0d35ed176..3764f18a6d692 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -48,9 +48,10 @@ type GenericMetricsRequest = Overwrite< } >; -export function transformDataToMetricsChart< - TRequest extends GenericMetricsRequest ->(result: ESSearchResponse, chartBase: ChartBase) { +export function transformDataToMetricsChart( + result: ESSearchResponse, + chartBase: ChartBase +) { const { aggregations, hits } = result; const timeseriesData = idx(aggregations, _ => _.timeseriesData); diff --git a/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts b/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts index 5557befa9d7e5..dbc0894ab8071 100644 --- a/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts +++ b/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts @@ -43,7 +43,9 @@ export const asChildFunctionRenderer = ( } public render() { - return this.props.children(this.getRendererArgs()); + return (this.props.children as ChildFunctionRendererProps['children'])( + this.getRendererArgs() + ); } private getRendererArgs = () => diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx index 11294bedbfff0..affb1accbbef4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx @@ -329,10 +329,10 @@ describe('IndexPattern Data Panel', () => { act(() => { ((inst.setProps as unknown) as (props: unknown) => {})({ ...props, - ...(propChanges || {}), + ...((propChanges as object) || {}), state: { ...props.state, - ...(stateChanges || {}), + ...((stateChanges as object) || {}), }, }); inst.update(); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts index 385d50215cd21..04dff6e0b4dc5 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts @@ -11,7 +11,6 @@ import { Subscription } from 'rxjs'; import { idx } from '@kbn/elastic-idx'; import { ml } from '../../services/ml_api_service'; import { getErrorMessage } from '../pages/analytics_management/hooks/use_create_analytics_form'; -import { RegressionEvaluateResponse } from '../common'; export type IndexName = string; export type IndexPattern = string; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts index c439c2bbf60eb..20e373a4168af 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts @@ -22,7 +22,7 @@ const mockCallEndpoint = sinon.stub(); const mockSearchRequest = {}; const mockConfig: ScrollConfig = { duration: '2s', size: 123 }; let realCancellationToken = new CancellationToken(); -let isCancelledStub: sinon.SinonStub; +let isCancelledStub: sinon.SinonStub<[], boolean>; describe('hitIterator', function() { beforeEach(() => { diff --git a/x-pack/package.json b/x-pack/package.json index 1c86147f1bc32..fabfce7a60c3b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -121,7 +121,7 @@ "cheerio": "0.22.0", "commander": "3.0.0", "copy-webpack-plugin": "^5.0.4", - "cypress": "^3.4.1", + "cypress": "^3.5.5", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.14.0", "enzyme-adapter-utils": "^1.12.0", @@ -171,7 +171,7 @@ "tmp": "0.1.0", "tree-kill": "^1.2.1", "ts-loader": "^6.0.4", - "typescript": "3.5.3", + "typescript": "3.7.2", "vinyl-fs": "^3.0.3", "whatwg-fetch": "^3.0.0", "xml-crypto": "^1.4.0", @@ -209,7 +209,7 @@ "apollo-client": "^2.3.8", "apollo-link": "^1.2.3", "apollo-link-error": "^1.1.7", - "apollo-link-http": "^1.5.4", + "apollo-link-http": "^1.5.16", "apollo-link-schema": "^1.1.0", "apollo-link-state": "^0.4.1", "apollo-server-errors": "^2.0.2", diff --git a/x-pack/typings/global_fetch.d.ts b/x-pack/typings/global_fetch.d.ts new file mode 100644 index 0000000000000..b2be5f7f24567 --- /dev/null +++ b/x-pack/typings/global_fetch.d.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. + */ + +// This type needs to still exist due to apollo-link-http-common hasn't yet updated +// it's usage (https://github.com/apollographql/apollo-link/issues/1131) +declare type GlobalFetch = WindowOrWorkerGlobalScope; diff --git a/yarn.lock b/yarn.lock index 6da8e810b6f56..5bb6bb3da2e47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3930,7 +3930,7 @@ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313" integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung== -"@types/sizzle@*": +"@types/sizzle@*", "@types/sizzle@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== @@ -4191,24 +4191,24 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.5.0.tgz#101d96743ce3365b3223df73d641078c9b775903" - integrity sha512-ddrJZxp5ns1Lh5ofZQYk3P8RyvKfyz/VcRR4ZiJLHO/ljnQAO8YvTfj268+WJOOadn99mvDiqJA65+HAKoeSPA== +"@typescript-eslint/eslint-plugin@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.7.0.tgz#dff176bdb73dfd7e2e43062452189bd1b9db6021" + integrity sha512-H5G7yi0b0FgmqaEUpzyBlVh0d9lq4cWG2ap0RKa6BkF3rpBb6IrAoubt1NWh9R2kRs/f0k6XwRDiDz3X/FqXhQ== dependencies: - "@typescript-eslint/experimental-utils" "2.5.0" + "@typescript-eslint/experimental-utils" "2.7.0" eslint-utils "^1.4.2" functional-red-black-tree "^1.0.1" regexpp "^2.0.1" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.5.0.tgz#383a97ded9a7940e5053449f6d73995e782b8fb1" - integrity sha512-UgcQGE0GKJVChyRuN1CWqDW8Pnu7+mVst0aWrhiyuUD1J9c+h8woBdT4XddCvhcXDodTDVIfE3DzGHVjp7tUeQ== +"@typescript-eslint/experimental-utils@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.7.0.tgz#58d790a3884df3041b5a5e08f9e5e6b7c41864b5" + integrity sha512-9/L/OJh2a5G2ltgBWJpHRfGnt61AgDeH6rsdg59BH0naQseSwR7abwHq3D5/op0KYD/zFT4LS5gGvWcMmegTEg== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.5.0" + "@typescript-eslint/typescript-estree" "2.7.0" eslint-scope "^5.0.0" "@typescript-eslint/experimental-utils@^1.13.0": @@ -4220,14 +4220,14 @@ "@typescript-eslint/typescript-estree" "1.13.0" eslint-scope "^4.0.0" -"@typescript-eslint/parser@^2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.5.0.tgz#858030ddd808fbbe88e03f42e5971efaccb8218a" - integrity sha512-9UBMiAwIDWSl79UyogaBdj3hidzv6exjKUx60OuZuFnJf56tq/UMpdPcX09YmGqE8f4AnAueYtBxV8IcAT3jdQ== +"@typescript-eslint/parser@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.7.0.tgz#b5e6a4944e2b68dba1e7fbfd5242e09ff552fd12" + integrity sha512-ctC0g0ZvYclxMh/xI+tyqP0EC2fAo6KicN9Wm2EIao+8OppLfxji7KAGJosQHSGBj3TcqUrA96AjgXuKa5ob2g== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.5.0" - "@typescript-eslint/typescript-estree" "2.5.0" + "@typescript-eslint/experimental-utils" "2.7.0" + "@typescript-eslint/typescript-estree" "2.7.0" eslint-visitor-keys "^1.1.0" "@typescript-eslint/typescript-estree@1.13.0": @@ -4238,16 +4238,17 @@ lodash.unescape "4.0.1" semver "5.5.0" -"@typescript-eslint/typescript-estree@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.5.0.tgz#40ada624d6217ef092a3a79ed30d947ad4f212ce" - integrity sha512-AXURyF8NcA3IsnbjNX1v9qbwa0dDoY9YPcKYR2utvMHoUcu3636zrz0gRWtVAyxbPCkhyKuGg6WZIyi2Fc79CA== +"@typescript-eslint/typescript-estree@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.7.0.tgz#34fd98c77a07b40d04d5b4203eddd3abeab909f4" + integrity sha512-vVCE/DY72N4RiJ/2f10PTyYekX2OLaltuSIBqeHYI44GQ940VCYioInIb8jKMrK9u855OEJdFC+HmWAZTnC+Ag== dependencies: debug "^4.1.1" glob "^7.1.4" is-glob "^4.0.1" lodash.unescape "4.0.1" semver "^6.3.0" + tsutils "^3.17.1" "@typescript-eslint/typescript-estree@^1.9.0": version "1.9.0" @@ -5125,20 +5126,23 @@ apollo-link-http-common@^0.2.13: ts-invariant "^0.3.2" tslib "^1.9.3" -apollo-link-http-common@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.4.tgz#877603f7904dc8f70242cac61808b1f8d034b2c3" - integrity sha512-4j6o6WoXuSPen9xh4NBaX8/vL98X1xY2cYzUEK1F8SzvHe2oFONfxJBTekwU8hnvapcuq8Qh9Uct+gelu8T10g== +apollo-link-http-common@^0.2.15: + version "0.2.15" + resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.15.tgz#304e67705122bf69a9abaded4351b10bc5efd6d9" + integrity sha512-+Heey4S2IPsPyTf8Ag3PugUupASJMW894iVps6hXbvwtg1aHSNMXUYO5VG7iRHkPzqpuzT4HMBanCTXPjtGzxg== dependencies: - apollo-link "^1.2.2" + apollo-link "^1.2.13" + ts-invariant "^0.4.0" + tslib "^1.9.3" -apollo-link-http@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.4.tgz#b80b7b4b342c655b6a5614624b076a36be368f43" - integrity sha512-e9Ng3HfnW00Mh3TI6DhNRfozmzQOtKgdi+qUAsHBOEcTP0PTAmb+9XpeyEEOueLyO0GXhB92HUCIhzrWMXgwyg== +apollo-link-http@^1.5.16: + version "1.5.16" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.16.tgz#44fe760bcc2803b8a7f57fc9269173afb00f3814" + integrity sha512-IA3xA/OcrOzINRZEECI6IdhRp/Twom5X5L9jMehfzEo2AXdeRwAMlH5LuvTZHgKD8V1MBnXdM6YXawXkTDSmJw== dependencies: - apollo-link "^1.2.2" - apollo-link-http-common "^0.2.4" + apollo-link "^1.2.13" + apollo-link-http-common "^0.2.15" + tslib "^1.9.3" apollo-link-schema@^1.1.0: version "1.1.0" @@ -5173,6 +5177,16 @@ apollo-link@^1.2.11: tslib "^1.9.3" zen-observable-ts "^0.8.18" +apollo-link@^1.2.13: + version "1.2.13" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.13.tgz#dff00fbf19dfcd90fddbc14b6a3f9a771acac6c4" + integrity sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw== + dependencies: + apollo-utilities "^1.3.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + zen-observable-ts "^0.8.20" + apollo-server-core@^1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.3.6.tgz#08636243c2de56fa8c267d68dd602cb1fbd323e3" @@ -5224,7 +5238,7 @@ apollo-utilities@^1.2.1: ts-invariant "^0.2.1" tslib "^1.9.3" -apollo-utilities@^1.3.2: +apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg== @@ -9178,13 +9192,14 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= -cypress@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.4.1.tgz#ca2e4e9864679da686c6a6189603efd409664c30" - integrity sha512-1HBS7t9XXzkt6QHbwfirWYty8vzxNMawGj1yI+Fu6C3/VZJ8UtUngMW6layqwYZzLTZV8tiDpdCNBypn78V4Dg== +cypress@^3.5.5: + version "3.6.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.6.1.tgz#4420957923879f60b7a5146ccbf81841a149b653" + integrity sha512-6n0oqENdz/oQ7EJ6IgESNb2M7Bo/70qX9jSJsAziJTC3kICfEMmJUlrAnP9bn+ut24MlXQST5nRXhUP5nRIx6A== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" "@cypress/xvfb" "1.2.4" + "@types/sizzle" "2.3.2" arch "2.1.1" bluebird "3.5.0" cachedir "1.3.0" @@ -9211,6 +9226,7 @@ cypress@^3.4.1: request-progress "3.0.0" supports-color "5.5.0" tmp "0.1.0" + untildify "3.0.3" url "0.11.0" yauzl "2.10.0" @@ -27891,10 +27907,10 @@ typescript-fsa@^2.0.0, typescript-fsa@^2.5.0: resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-2.5.0.tgz#1baec01b5e8f5f34c322679d1327016e9e294faf" integrity sha1-G67AG16PXzTDImedEycBbp4pT68= -typescript@3.5.3, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.3.3333, typescript@~3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" - integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== +typescript@3.5.3, typescript@3.7.2, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.3.3333, typescript@~3.5.3: + version "3.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" + integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== typings-tester@^0.3.2: version "0.3.2" @@ -28213,6 +28229,11 @@ unstated@^2.1.1: dependencies: create-react-context "^0.1.5" +untildify@3.0.3, untildify@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" + integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== + untildify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0" @@ -28220,11 +28241,6 @@ untildify@^2.0.0: dependencies: os-homedir "^1.0.0" -untildify@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" - integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== - unzip-response@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" @@ -30505,6 +30521,14 @@ zen-observable-ts@^0.8.18: tslib "^1.9.3" zen-observable "^0.8.0" +zen-observable-ts@^0.8.20: + version "0.8.20" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz#44091e335d3fcbc97f6497e63e7f57d5b516b163" + integrity sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA== + dependencies: + tslib "^1.9.3" + zen-observable "^0.8.0" + zen-observable@^0.8.0: version "0.8.8" resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.8.tgz#1ea93995bf098754a58215a1e0a7309e5749ec42" From 1a8bb3a2231f1809d32b8ca2070b145c13c84fec Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Fri, 15 Nov 2019 10:05:58 +0000 Subject: [PATCH 09/44] [ML] Fixes word wrap in Overview page sidebar on IE (#50668) --- .../ml/public/overview/components/sidebar.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx b/x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx index 496beb158f698..82caadd4d58ea 100644 --- a/x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx +++ b/x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx @@ -25,13 +25,13 @@ function getCreateJobLink(createAnomalyDetectionJobDisabled: boolean) { return createAnomalyDetectionJobDisabled === true ? ( ) : ( ); @@ -43,15 +43,13 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }

@@ -87,13 +85,13 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }

), From 0c42aa8a58f88ae14d7e3efda65f694a92e0c729 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 15 Nov 2019 12:56:24 +0100 Subject: [PATCH 10/44] [ML] Add the job message tab to data frame analytics (#50468) * [ML] get analytics job messages from ml-notifications * [ML] remove commented import * [ML] add job_type filter for anomaly_detector * [ML] job messages component * [ML] update styles * [ML] update job messages container for anomaly detection with async await * [ML] fix passing a prop * [ML] fix types and error message * [ML] fix i18n * [ML] fix text alignment * [ML] remove duplicated copyright comment --- .../ml/common/constants/index_patterns.ts | 1 - .../plugins/ml/common/types/audit_message.ts | 4 - .../job_message_icon/{index.js => index.ts} | 1 - .../job_message_icon/job_message_icon.tsx | 2 +- .../public/components/job_messages/index.ts | 7 ++ .../components/job_messages/job_messages.tsx | 73 +++++++++++++++ .../analytics_list/expanded_row.tsx | 7 +- .../expanded_row_messages_pane.tsx | 56 ++---------- .../components/job_details/_job_details.scss | 45 +++------- .../components/job_details/job_details.js | 2 +- .../job_details/job_messages_pane.js | 89 ------------------- .../job_details/job_messages_pane.tsx | 40 +++++++++ .../jobs_list_page/_expanded_row.scss | 44 +++------ .../public/services/ml_api_service/index.d.ts | 3 +- .../analytics_audit_messages.ts | 19 ++-- .../job_audit_messages/job_audit_messages.js | 5 ++ .../translations/translations/ja-JP.json | 6 -- .../translations/translations/zh-CN.json | 6 -- 18 files changed, 176 insertions(+), 234 deletions(-) rename x-pack/legacy/plugins/ml/public/components/job_message_icon/{index.js => index.ts} (99%) create mode 100644 x-pack/legacy/plugins/ml/public/components/job_messages/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.js create mode 100644 x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx diff --git a/x-pack/legacy/plugins/ml/common/constants/index_patterns.ts b/x-pack/legacy/plugins/ml/common/constants/index_patterns.ts index 9b87f69ad1b6b..4a0dfdde04ad4 100644 --- a/x-pack/legacy/plugins/ml/common/constants/index_patterns.ts +++ b/x-pack/legacy/plugins/ml/common/constants/index_patterns.ts @@ -10,4 +10,3 @@ export const ML_ANNOTATIONS_INDEX_PATTERN = '.ml-annotations-6'; export const ML_RESULTS_INDEX_PATTERN = '.ml-anomalies-*'; export const ML_NOTIFICATION_INDEX_PATTERN = '.ml-notifications*'; -export const ML_DF_NOTIFICATION_INDEX_PATTERN = '.data-frame-notifications-1'; diff --git a/x-pack/legacy/plugins/ml/common/types/audit_message.ts b/x-pack/legacy/plugins/ml/common/types/audit_message.ts index 58e77495b9093..a0ac92417dfe6 100644 --- a/x-pack/legacy/plugins/ml/common/types/audit_message.ts +++ b/x-pack/legacy/plugins/ml/common/types/audit_message.ts @@ -12,10 +12,6 @@ export interface AuditMessageBase { text?: string; } -export interface AnalyticsMessage extends AuditMessageBase { - analytics_id: string; -} - export interface JobMessage extends AuditMessageBase { job_id: string; } diff --git a/x-pack/legacy/plugins/ml/public/components/job_message_icon/index.js b/x-pack/legacy/plugins/ml/public/components/job_message_icon/index.ts similarity index 99% rename from x-pack/legacy/plugins/ml/public/components/job_message_icon/index.js rename to x-pack/legacy/plugins/ml/public/components/job_message_icon/index.ts index a92fd6c8ae058..c307df90ae8ce 100644 --- a/x-pack/legacy/plugins/ml/public/components/job_message_icon/index.js +++ b/x-pack/legacy/plugins/ml/public/components/job_message_icon/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - export { JobIcon } from './job_message_icon'; diff --git a/x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx b/x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx index 59e71974d0aed..545e9231699fd 100644 --- a/x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx +++ b/x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx @@ -11,7 +11,7 @@ import { AuditMessageBase } from '../../../common/types/audit_message'; interface Props { message: AuditMessageBase; - showTooltip: boolean; + showTooltip?: boolean; } const [INFO, WARNING, ERROR] = ['info', 'warning', 'error']; diff --git a/x-pack/legacy/plugins/ml/public/components/job_messages/index.ts b/x-pack/legacy/plugins/ml/public/components/job_messages/index.ts new file mode 100644 index 0000000000000..dee6aa66694a6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/job_messages/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { JobMessages } from './job_messages'; diff --git a/x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx b/x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx new file mode 100644 index 0000000000000..08f9a4379559b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx @@ -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 React, { FC } from 'react'; + +import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; +// @ts-ignore +import { formatDate } from '@elastic/eui/lib/services/format'; +import { i18n } from '@kbn/i18n'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; + +import { JobMessage } from '../../../common/types/audit_message'; +import { JobIcon } from '../job_message_icon'; + +const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + +interface JobMessagesProps { + messages: JobMessage[]; + loading: boolean; + error: string; +} + +/** + * Component for rendering job messages for anomaly detection + * and data frame analytics jobs. + */ +export const JobMessages: FC = ({ messages, loading, error }) => { + const columns = [ + { + name: '', + render: (message: JobMessage) => , + width: `${theme.euiSizeL}`, + }, + { + name: i18n.translate('xpack.ml.jobMessages.timeLabel', { + defaultMessage: 'Time', + }), + render: (message: any) => formatDate(message.timestamp, TIME_FORMAT), + width: '120px', + }, + { + field: 'node_name', + name: i18n.translate('xpack.ml.jobMessages.nodeLabel', { + defaultMessage: 'Node', + }), + width: '150px', + }, + { + field: 'message', + name: i18n.translate('xpack.ml.jobMessages.messageLabel', { + defaultMessage: 'Message', + }), + width: '50%', + }, + ]; + + return ( + <> + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index bfa21e503d5aa..2ad81a05741c1 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -27,7 +27,7 @@ import { } from '../../../../common'; import { isCompletedAnalyticsJob } from './common'; import { isRegressionAnalysis } from '../../../../common/analytics'; -// import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; +import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; function getItemDescription(value: any) { if (typeof value === 'object') { @@ -235,19 +235,16 @@ export const ExpandedRow: FC = ({ item }) => { name: 'JSON', content: , }, - // Audit messages are not yet supported by the analytics API. - /* { id: 'ml-analytics-job-messages', name: i18n.translate( 'xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsMessagesLabel', { - defaultMessage: 'Messages', + defaultMessage: 'Job messages', } ), content: , }, - */ ]; // Using `expand=false` here so the tabs themselves don't spread diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx index 1751cf1a39646..e639f32116d4a 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx @@ -4,27 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState } from 'react'; - -import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; -// @ts-ignore -import { formatDate } from '@elastic/eui/lib/services/format'; +import React, { FC, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; import { ml } from '../../../../../services/ml_api_service'; -// @ts-ignore -import { JobIcon } from '../../../../../components/job_message_icon'; -import { AnalyticsMessage } from '../../../../../../common/types/audit_message'; import { useRefreshAnalyticsList } from '../../../../common'; - -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +import { JobMessages } from '../../../../../components/job_messages'; +import { JobMessage } from '../../../../../../common/types/audit_message'; interface Props { analyticsId: string; } export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { - const [messages, setMessages] = useState([]); + const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); @@ -63,43 +55,5 @@ export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { useRefreshAnalyticsList({ onRefresh: getMessagesFactory() }); - const columns = [ - { - name: '', - render: (message: AnalyticsMessage) => , - width: `${theme.euiSizeXL}px`, - }, - { - name: i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.timeLabel', { - defaultMessage: 'Time', - }), - render: (message: any) => formatDate(message.timestamp, TIME_FORMAT), - }, - { - field: 'node_name', - name: i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.nodeLabel', { - defaultMessage: 'Node', - }), - }, - { - field: 'message', - name: i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.messageLabel', { - defaultMessage: 'Message', - }), - width: '50%', - }, - ]; - - return ( - - - - - ); + return ; }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss index 022c3513d09e1..1f68ec67ded47 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss @@ -1,10 +1,9 @@ - .tab-contents { margin: -$euiSizeS; padding: $euiSizeS; background-color: $euiColorEmptyShade; - // SASSTODO: Need to remove bootstrap grid + // SASSTODO: Need to remove bootstrap grid .col-md-6:nth-child(1) { // SASSTODO: Why is this 7? padding-right: 7px; @@ -65,40 +64,24 @@ } } - // SASSTODO: This needs a proper calc + // SASSTODO: This needs a proper calc .json-textarea { height: 500px; } +} - // SASSTODO: This needs to be rewritten. A lot of this should be done with the JS props - .job-messages-table { - max-height: 500px; - overflow: auto; +.job-messages-table { + max-height: 500px; + overflow: auto; + text-align: left; - .euiTable { - font-size: 12px; - - th:nth-child(1) { - width: $euiSizeXL; - } - th:nth-child(2) { - width: 150px; - } - th:nth-child(3) { - width: 120px; - } - th:nth-child(4) { - width: auto; - } - tr:last-child { - td { - border-bottom: none; - } - } - - .euiTableRowCell { - background-color: $euiColorEmptyShade; - } + tr:last-child { + td { + border-bottom: none; } } + + .euiTableRowCell { + background-color: $euiColorEmptyShade; + } } diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js index 192310937c0e9..acec94efc3789 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js @@ -118,7 +118,7 @@ class JobDetailsUI extends Component { id: 'xpack.ml.jobsList.jobDetails.tabs.jobMessagesLabel', defaultMessage: 'Job messages' }), - content: , + content: , }, ]; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.js deleted file mode 100644 index 8015c38cfc805..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.js +++ /dev/null @@ -1,89 +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 PropTypes from 'prop-types'; -import React, { - Component -} from 'react'; - -import { - EuiSpacer, - EuiBasicTable, -} from '@elastic/eui'; - -import { formatDate } from '@elastic/eui/lib/services/format'; -import { ml } from 'plugins/ml/services/ml_api_service'; -import { JobIcon } from '../../../../components/job_message_icon'; -import { injectI18n } from '@kbn/i18n/react'; - -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; - -class JobMessagesPaneUI extends Component { - - constructor(props) { - super(props); - - this.state = { - messages: [] - }; - this.jobId = props.job.job_id; - } - - componentDidMount() { - ml.jobs.jobAuditMessages(this.jobId) - .then((messages) => { - this.setState({ messages }); - }) - .catch((error) => { - console.log('Job messages could not be loaded', error); - }); - } - - render() { - const { messages } = this.state; - const { intl } = this.props; - const columns = [{ - name: '', - render: item => () - }, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.messagesPane.timeLabel', - defaultMessage: 'Time' - }), - render: item => formatDate(item.timestamp, TIME_FORMAT) - }, { - field: 'node_name', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.messagesPane.nodeLabel', - defaultMessage: 'Node' - }), - }, { - field: 'message', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.messagesPane.messageLabel', - defaultMessage: 'Message' - }), - } - ]; - return ( - - -

- -
- - ); - } -} -JobMessagesPaneUI.propTypes = { - job: PropTypes.object.isRequired, -}; - -export const JobMessagesPane = injectI18n(JobMessagesPaneUI); diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx new file mode 100644 index 0000000000000..ca80012767c2d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -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 React, { FC, useEffect, useState } from 'react'; + +import { ml } from '../../../../services/ml_api_service'; +import { JobMessages } from '../../../../components/job_messages'; +import { JobMessage } from '../../../../../common/types/audit_message'; + +interface JobMessagesPaneProps { + jobId: string; +} + +export const JobMessagesPane: FC = ({ jobId }) => { + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const fetchMessages = async () => { + setIsLoading(true); + try { + setMessages(await ml.jobs.jobAuditMessages(jobId)); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + setErrorMessage(e); + // eslint-disable-next-line no-console + console.error('Job messages could not be loaded', e); + } + }; + + useEffect(() => { + fetchMessages(); + }, []); + + return ; +}; diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss index e21d7d6c0e1db..aeddbbd1a5108 100644 --- a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss +++ b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss @@ -4,7 +4,7 @@ padding: $euiSizeS; background-color: $euiColorEmptyShade; - // SASSTODO: Need to remove bootstrap grid + // SASSTODO: Need to remove bootstrap grid .col-md-6:nth-child(1) { // SASSTODO: Why is this 7? padding-right: 7px; @@ -65,40 +65,24 @@ } } - // SASSTODO: This needs a proper calc + // SASSTODO: This needs a proper calc .json-textarea { height: 500px; } +} - // SASSTODO: This needs to be rewritten. A lot of this should be done with the JS props - .job-messages-table { - max-height: 500px; - overflow: auto; - - .euiTable { - font-size: 12px; - - th:nth-child(1) { - width: $euiSizeXL; - } - th:nth-child(2) { - width: 150px; - } - th:nth-child(3) { - width: 120px; - } - th:nth-child(4) { - width: auto; - } - tr:last-child { - td { - border-bottom: none; - } - } +.job-messages-table { + max-height: 500px; + overflow: auto; + text-align: left; - .euiTableRowCell { - background-color: $euiColorEmptyShade; - } + tr:last-child { + td { + border-bottom: none; } } + + .euiTableRowCell { + background-color: $euiColorEmptyShade; + } } diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts index 38a71d994c601..414229578c217 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts @@ -12,6 +12,7 @@ import { MlSummaryJobs } from '../../../common/types/jobs'; import { MlServerDefaults, MlServerLimits } from '../../services/ml_server_info'; import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { JobMessage } from '../../../common/types/audit_message'; // TODO This is not a complete representation of all methods of `ml.*`. // It just satisfies needs for other parts of the code area which use @@ -117,7 +118,7 @@ declare interface Ml { stopDatafeeds(datafeedIds: string[]): Promise; deleteJobs(jobIds: string[]): Promise; closeJobs(jobIds: string[]): Promise; - jobAuditMessages(jobId: string, from: string): Promise; + jobAuditMessages(jobId: string, from?: string): Promise; deletingJobTasks(): Promise; newJobCaps(indexPatternTitle: string, isRollup: boolean): Promise; newJobLineChart( diff --git a/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts b/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts index c0a4e639f6434..abe389165182f 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts +++ b/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ML_DF_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { callWithRequestType } from '../../../common/types/kibana'; -import { AnalyticsMessage } from '../../../common/types/audit_message'; +import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { JobMessage } from '../../../common/types/audit_message'; const SIZE = 50; @@ -15,7 +15,7 @@ interface Message { _type: string; _id: string; _score: null | number; - _source: AnalyticsMessage; + _source: JobMessage; sort?: any; } @@ -37,6 +37,11 @@ export function analyticsAuditMessagesProvider(callWithRequest: callWithRequestT level: 'activity', }, }, + must: { + term: { + job_type: 'data_frame_analytics', + }, + }, }, }, ], @@ -50,12 +55,12 @@ export function analyticsAuditMessagesProvider(callWithRequest: callWithRequestT should: [ { term: { - analytics_id: '', // catch system messages + job_id: '', // catch system messages }, }, { term: { - analytics_id: analyticsId, // messages for specified analyticsId + job_id: analyticsId, // messages for specified analyticsId }, }, ], @@ -65,12 +70,12 @@ export function analyticsAuditMessagesProvider(callWithRequest: callWithRequestT try { const resp = await callWithRequest('search', { - index: ML_DF_NOTIFICATION_INDEX_PATTERN, + index: ML_NOTIFICATION_INDEX_PATTERN, ignore_unavailable: true, rest_total_hits_as_int: true, size: SIZE, body: { - sort: [{ timestamp: { order: 'desc' } }, { analytics_id: { order: 'asc' } }], + sort: [{ timestamp: { order: 'desc' } }, { job_id: { order: 'asc' } }], query, }, }); diff --git a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index 891990a84756a..3b666cad7d8eb 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -48,6 +48,11 @@ export function jobAuditMessagesProvider(callWithRequest) { term: { level: 'activity' } + }, + must: { + term: { + job_type: 'anomaly_detector' + } } } }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 34491036e5a45..872e09ea97d3b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6130,9 +6130,6 @@ "xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel": "表示", "xpack.ml.jobsList.jobDetails.generalTitle": "一般", "xpack.ml.jobsList.jobDetails.influencersTitle": "影響", - "xpack.ml.jobsList.jobDetails.messagesPane.messageLabel": "メッセージ", - "xpack.ml.jobsList.jobDetails.messagesPane.nodeLabel": "ノード", - "xpack.ml.jobsList.jobDetails.messagesPane.timeLabel": "時間", "xpack.ml.jobsList.jobDetails.modelSizeStatsTitle": "モデルサイズ統計", "xpack.ml.jobsList.jobDetails.nodeTitle": "ノード", "xpack.ml.jobsList.jobDetails.noPermissionToViewDatafeedPreviewTitle": "データフィードのプレビューを表示するパーミッションがありません", @@ -6610,9 +6607,6 @@ "xpack.ml.datavisualizer.searchPanel.sampleLabel": "サンプル", "xpack.ml.datavisualizer.searchPanel.sampleSizeAriaLabel": "サンプリングするドキュメント数を選択してください", "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage": "メッセージを読み込めませんでした", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.messageLabel": "メッセージ", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.nodeLabel": "ノード", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.timeLabel": "時間", "xpack.ml.fieldDataCard.cardBoolean.documentsCountDescription": "{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardDate.documentsCountDescription": "{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardDate.earliestDescription": "最も古い {earliestFormatted}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index df00be9685ccf..25ad15d05ea16 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6131,9 +6131,6 @@ "xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel": "查看", "xpack.ml.jobsList.jobDetails.generalTitle": "常规", "xpack.ml.jobsList.jobDetails.influencersTitle": "影响因素", - "xpack.ml.jobsList.jobDetails.messagesPane.messageLabel": "消息", - "xpack.ml.jobsList.jobDetails.messagesPane.nodeLabel": "节点", - "xpack.ml.jobsList.jobDetails.messagesPane.timeLabel": "时间", "xpack.ml.jobsList.jobDetails.modelSizeStatsTitle": "模型大小统计", "xpack.ml.jobsList.jobDetails.nodeTitle": "节点", "xpack.ml.jobsList.jobDetails.noPermissionToViewDatafeedPreviewTitle": "您无权查看数据馈送预览", @@ -6703,9 +6700,6 @@ "xpack.ml.datavisualizer.searchPanel.sampleLabel": "采样", "xpack.ml.datavisualizer.searchPanel.sampleSizeAriaLabel": "选择要采样的文档数目", "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage": "无法加载消息", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.messageLabel": "消息", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.nodeLabel": "节点", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.timeLabel": "时间", "xpack.ml.fieldDataCard.cardBoolean.documentsCountDescription": "{count, plural, zero {# 个文档} one {# 个文档} other {# 个文档}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardDate.documentsCountDescription": "{count, plural, zero {# 个文档} one {# 个文档} other {# 个文档}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardDate.earliestDescription": "最早的 {earliestFormatted}", From c982b8f4dcf6d5292cd48b932b9fdb29a81fccf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 15 Nov 2019 13:26:57 +0100 Subject: [PATCH 11/44] [APM] Update Error occurrences graph tooltip to display start and end for bucket period (#49638) * Adding end time inside error tooltip * changing end time precision * refactoring * refactoring * pr comments refactoring * pr comments refactoring * pr comments refactoring * pr comments refactoring * renaming some functions to make it more clear * Refactoring date difference range * refactoring transformers file * refactoring date time formatters * refactoring formatters into a new folder * refactoring getDurationUnit * refactoring duration formatter * fixing unit test * refactoring unit test * Adding timezone to tests * fixing translation issue * fixing translation issue * improving code * exporting toMicroseconds * removing unused import * refactoring duration * refactoring duration * fixing unit test * fixing unit test --- .../ErrorGroupDetails/Distribution/index.tsx | 5 + .../app/ErrorGroupOverview/List/index.tsx | 2 +- .../app/ServiceOverview/ServiceList/index.tsx | 8 +- .../AgentConfigurationList.tsx | 2 +- .../app/TraceOverview/TraceList.tsx | 8 +- .../TransactionDetails/Distribution/index.tsx | 17 +- .../Waterfall/WaterfallItem.tsx | 4 +- .../app/TransactionOverview/List/index.tsx | 12 +- .../shared/Summary/DurationSummaryItem.tsx | 4 +- .../components/shared/Summary/index.tsx | 3 +- .../TimestampTooltip/__test__/index.test.tsx | 61 +++++ .../shared/TimestampTooltip/index.test.tsx | 108 -------- .../shared/TimestampTooltip/index.tsx | 36 +-- .../TransactionBreakdownGraph/index.tsx | 3 +- .../Histogram/__test__/Histogram.test.js | 18 +- .../shared/charts/MetricsChart/index.tsx | 23 +- .../shared/charts/Timeline/AgentMarker.js | 4 +- .../shared/charts/Timeline/TimelineAxis.js | 9 +- .../components/shared/charts/Tooltip/index.js | 6 +- .../ChoroplethMap/ChoroplethToolTip.tsx | 4 +- .../shared/charts/TransactionCharts/index.tsx | 12 +- .../apm/public/selectors/chartSelectors.ts | 8 +- .../plugins/apm/public/utils/flattenObject.ts | 3 +- .../plugins/apm/public/utils/formatters.ts | 247 ------------------ .../formatters/__test__/datetime.test.ts | 146 +++++++++++ .../formatters/__test__/duration.test.ts | 131 ++++++++++ .../formatters/__test__/formatters.test.ts | 31 +++ .../__test__/size.test.ts} | 56 +--- .../apm/public/utils/formatters/datetime.ts | 149 +++++++++++ .../apm/public/utils/formatters/duration.ts | 153 +++++++++++ .../apm/public/utils/formatters/formatters.ts | 38 +++ .../apm/public/utils/formatters/index.ts | 10 + .../apm/public/utils/formatters/size.ts | 67 +++++ .../public/utils/isValidCoordinateValue.ts | 6 +- x-pack/legacy/plugins/apm/typings/common.d.ts | 2 + .../legacy/plugins/apm/typings/timeseries.ts | 3 +- 36 files changed, 889 insertions(+), 510 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts rename x-pack/legacy/plugins/apm/public/utils/{__test__/formatters.test.ts => formatters/__test__/size.test.ts} (63%) create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/index.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/size.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 2667d03ef8dde..daba164a4a00c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -10,6 +10,7 @@ import React from 'react'; // @ts-ignore import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; +import { asRelativeDateTimeRange } from '../../../../utils/formatters'; interface IBucket { key: number; @@ -51,6 +52,9 @@ interface Props { title: React.ReactNode; } +const tooltipHeader = (bucket: FormattedBucket) => + asRelativeDateTimeRange(bucket.x0, bucket.x); + export function ErrorDistribution({ distribution, title }: Props) { const buckets = getFormattedBuckets( distribution.buckets, @@ -73,6 +77,7 @@ export function ErrorDistribution({ distribution, title }: Props) { {title} bucket.x} xType="time" buckets={buckets} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index 2f06f1d52de6b..a6c8058158578 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -143,7 +143,7 @@ const ErrorGroupList: React.FC = props => { align: 'right', render: (value?: number) => value ? ( - + ) : ( NOT_AVAILABLE_LABEL ) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index f2524ef1c16f4..13e7a5bfd894e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { fontSizes, truncate } from '../../../../style/variables'; -import { asDecimal, asMillis } from '../../../../utils/formatters'; +import { asDecimal, convertTo } from '../../../../utils/formatters'; import { ManagedTable } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; @@ -80,7 +80,11 @@ export const SERVICE_COLUMNS = [ }), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx index 161d371148478..c660455e1eed8 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx @@ -128,7 +128,7 @@ export function AgentConfigurationList({ ), sortable: true, render: (value: number) => ( - + ) }, { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index ca10b06c11cbf..9116e02870a80 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -10,7 +10,7 @@ import React from 'react'; import styled from 'styled-components'; import { ITransactionGroup } from '../../../../server/lib/transaction_groups/transform'; import { fontSizes, truncate } from '../../../style/variables'; -import { asMillis } from '../../../utils/formatters'; +import { convertTo } from '../../../utils/formatters'; import { EmptyMessage } from '../../shared/EmptyMessage'; import { ImpactBar } from '../../shared/ImpactBar'; import { TransactionDetailLink } from '../../shared/Links/apm/TransactionDetailLink'; @@ -66,7 +66,11 @@ const traceListColumns: Array> = [ }), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index fc86f4bb78afb..c9e5175a10921 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -11,7 +11,7 @@ import React, { FunctionComponent, useCallback } from 'react'; import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { getTimeFormatter, timeUnit } from '../../../../utils/formatters'; +import { getDurationFormatter } from '../../../../utils/formatters'; // @ts-ignore import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; @@ -132,8 +132,7 @@ export const TransactionDistribution: FunctionComponent = ( ); const xMax = d3.max(buckets, d => d.x) || 0; - const timeFormatter = getTimeFormatter(xMax); - const unit = timeUnit(xMax); + const timeFormatter = getDurationFormatter(xMax); const bucketIndex = buckets.findIndex( bucket => @@ -187,18 +186,18 @@ export const TransactionDistribution: FunctionComponent = ( }); } }} - formatX={timeFormatter} + formatX={(time: number) => timeFormatter(time).formatted} formatYShort={formatYShort} formatYLong={formatYLong} verticalLineHover={(bucket: IChartPoint) => bucket.y > 0 && !bucket.sample } backgroundHover={(bucket: IChartPoint) => bucket.y > 0 && bucket.sample} - tooltipHeader={(bucket: IChartPoint) => - `${timeFormatter(bucket.x0, { - withUnit: false - })} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}` - } + tooltipHeader={(bucket: IChartPoint) => { + const xFormatted = timeFormatter(bucket.x); + const x0Formatted = timeFormatter(bucket.x0); + return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; + }} tooltipFooter={(bucket: IChartPoint) => !bucket.sample && i18n.translate( diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index a5e6eb622e8fb..c64231a6ded86 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -12,7 +12,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; -import { asTime } from '../../../../../../utils/formatters'; +import { asDuration } from '../../../../../../utils/formatters'; import { ErrorCountBadge } from '../../ErrorCountBadge'; import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; @@ -133,7 +133,7 @@ const SpanActionToolTip: React.SFC = ({ function Duration({ item }: { item: IWaterfallItem }) { return ( - {asTime(item.duration)} + {asDuration(item.duration)} ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx index 062d103bfc448..3d75011f52f19 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/transform'; import { fontFamilyCode, truncate } from '../../../../style/variables'; -import { asDecimal, asMillis } from '../../../../utils/formatters'; +import { asDecimal, convertTo } from '../../../../utils/formatters'; import { ImpactBar } from '../../../shared/ImpactBar'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; @@ -28,6 +28,12 @@ interface Props { isLoading: boolean; } +const toMilliseconds = (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted; + export function TransactionList({ items, isLoading }: Props) { const columns: Array> = useMemo( () => [ @@ -67,7 +73,7 @@ export function TransactionList({ items, isLoading }: Props) { ), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => toMilliseconds(time) }, { field: 'p95', @@ -79,7 +85,7 @@ export function TransactionList({ items, isLoading }: Props) { ), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => toMilliseconds(time) }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx index c76e62d987aac..a5a677296825c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip, EuiText } from '@elastic/eui'; import { PercentOfParent } from '../../app/TransactionDetails/WaterfallWithSummmary/PercentOfParent'; -import { asTime } from '../../../utils/formatters'; +import { asDuration } from '../../../utils/formatters'; interface Props { duration: number; @@ -29,7 +29,7 @@ const DurationSummaryItem = ({ return ( <> - {asTime(duration)} + {asDuration(duration)}   diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx index c4b750a360efd..ce6935d1858aa 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx @@ -8,9 +8,10 @@ import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { px, units } from '../../../../public/style/variables'; +import { Maybe } from '../../../../typings/common'; interface Props { - items: Array; + items: Array>; } // TODO: Light/Dark theme (@see https://github.com/elastic/kibana/issues/44840) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx new file mode 100644 index 0000000000000..b4678b287dc16 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx @@ -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 { shallow } from 'enzyme'; +import React from 'react'; +import moment from 'moment-timezone'; +import { TimestampTooltip } from '../index'; +import { mockNow } from '../../../../utils/testHelpers'; + +describe('TimestampTooltip', () => { + const timestamp = 1570720000123; // Oct 10, 2019, 08:06:40.123 (UTC-7) + + beforeAll(() => { + // mock Date.now + mockNow(1570737000000); + + moment.tz.setDefault('America/Los_Angeles'); + }); + + afterAll(() => moment.tz.setDefault('')); + + it('should render component with relative time in body and absolute time in tooltip', () => { + expect(shallow()) + .toMatchInlineSnapshot(` + + 5 hours ago + + `); + }); + + it('should format with precision in milliseconds by default', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06:40.123 (UTC-7)'); + }); + + it('should format with precision in seconds', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06:40 (UTC-7)'); + }); + + it('should format with precision in minutes', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06 (UTC-7)'); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx deleted file mode 100644 index a7149c7604695..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx +++ /dev/null @@ -1,108 +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 { shallow } from 'enzyme'; -import React from 'react'; -import moment from 'moment-timezone'; -import { TimestampTooltip, asAbsoluteTime } from './index'; -import { mockNow } from '../../../utils/testHelpers'; - -describe('asAbsoluteTime', () => { - afterAll(() => moment.tz.setDefault('')); - - it('should add a leading plus for timezones with positive UTC offset', () => { - moment.tz.setDefault('Europe/Copenhagen'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 14:00 (UTC+2)' - ); - }); - - it('should add a leading minus for timezones with negative UTC offset', () => { - moment.tz.setDefault('America/Los_Angeles'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 05:00 (UTC-7)' - ); - }); - - it('should use default UTC offset formatting when offset contains minutes', () => { - moment.tz.setDefault('Canada/Newfoundland'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 09:30 (UTC-02:30)' - ); - }); - - it('should respect DST', () => { - moment.tz.setDefault('Europe/Copenhagen'); - const timeWithDST = 1559390400000; // Jun 1, 2019 - const timeWithoutDST = 1575201600000; // Dec 1, 2019 - - expect(asAbsoluteTime({ time: timeWithDST })).toBe( - 'Jun 1, 2019, 14:00:00.000 (UTC+2)' - ); - - expect(asAbsoluteTime({ time: timeWithoutDST })).toBe( - 'Dec 1, 2019, 13:00:00.000 (UTC+1)' - ); - }); -}); - -describe('TimestampTooltip', () => { - const timestamp = 1570720000123; // Oct 10, 2019, 08:06:40.123 (UTC-7) - - beforeAll(() => { - // mock Date.now - mockNow(1570737000000); - - moment.tz.setDefault('America/Los_Angeles'); - }); - - afterAll(() => moment.tz.setDefault('')); - - it('should render component with relative time in body and absolute time in tooltip', () => { - expect(shallow()) - .toMatchInlineSnapshot(` - - 5 hours ago - - `); - }); - - it('should format with precision in milliseconds by default', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06:40.123 (UTC-7)'); - }); - - it('should format with precision in seconds', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06:40 (UTC-7)'); - }); - - it('should format with precision in minutes', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06 (UTC-7)'); - }); - - it('should format with precision in days', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019 (UTC-7)'); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx index d7ef6517c2fb8..504ff36c078f0 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx @@ -6,48 +6,20 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; import moment from 'moment-timezone'; +import { asAbsoluteDateTime, TimeUnit } from '../../../utils/formatters'; interface Props { /** * timestamp in milliseconds */ time: number; - precision?: 'days' | 'minutes' | 'seconds' | 'milliseconds'; + timeUnit?: TimeUnit; } -function getPreciseTime(precision: Props['precision']) { - switch (precision) { - case 'days': - return ''; - case 'minutes': - return ', HH:mm'; - case 'seconds': - return ', HH:mm:ss'; - default: - return ', HH:mm:ss.SSS'; - } -} - -function withLeadingPlus(value: number) { - return value > 0 ? `+${value}` : value; -} - -export function asAbsoluteTime({ time, precision = 'milliseconds' }: Props) { - const momentTime = moment(time); - const utcOffsetHours = momentTime.utcOffset() / 60; - const utcOffsetFormatted = Number.isInteger(utcOffsetHours) - ? withLeadingPlus(utcOffsetHours) - : 'Z'; - - return momentTime.format( - `MMM D, YYYY${getPreciseTime(precision)} (UTC${utcOffsetFormatted})` - ); -} - -export function TimestampTooltip({ time, precision = 'milliseconds' }: Props) { +export function TimestampTooltip({ time, timeUnit = 'milliseconds' }: Props) { const momentTime = moment(time); const relativeTimeLabel = momentTime.fromNow(); - const absoluteTimeLabel = asAbsoluteTime({ time, precision }); + const absoluteTimeLabel = asAbsoluteDateTime(time, timeUnit); return ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index 1bcf4e08c9144..c4e7ed86df8b7 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -9,6 +9,7 @@ import numeral from '@elastic/numeral'; import { throttle } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; +import { Maybe } from '../../../../../typings/common'; import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; import { asPercent } from '../../../../utils/formatters'; import { unit } from '../../../../style/variables'; @@ -19,7 +20,7 @@ interface Props { timeseries: TimeSeries[]; } -const tickFormatY = (y: number | null | undefined) => { +const tickFormatY = (y: Maybe) => { return numeral(y || 0).format('0 %'); }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js index b511bdc439227..f76a27480137a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js @@ -11,9 +11,8 @@ import d3 from 'd3'; import { HistogramInner } from '../index'; import response from './response.json'; import { - getTimeFormatter, asDecimal, - timeUnit + getDurationFormatter } from '../../../../../utils/formatters'; import { toJson } from '../../../../../utils/testHelpers'; import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index'; @@ -25,8 +24,7 @@ describe('Histogram', () => { beforeEach(() => { const buckets = getFormattedBuckets(response.buckets, response.bucketSize); const xMax = d3.max(buckets, d => d.x); - const timeFormatter = getTimeFormatter(xMax); - const unit = timeUnit(xMax); + const timeFormatter = getDurationFormatter(xMax); wrapper = mount( { bucketSize={response.bucketSize} transactionId="myTransactionId" onClick={onClick} - formatX={timeFormatter} + formatX={time => timeFormatter(time).formatted} formatYShort={t => `${asDecimal(t)} occ.`} formatYLong={t => `${asDecimal(t)} occurrences`} - tooltipHeader={bucket => - `${timeFormatter(bucket.x0, { - withUnit: false - })} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}` - } + tooltipHeader={bucket => { + const xFormatted = timeFormatter(bucket.x); + const x0Formatted = timeFormatter(bucket.x0); + return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; + }} width={800} /> ); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx index 51aa4a40fb923..30dcc99af31b9 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -9,20 +9,21 @@ import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform // @ts-ignore import CustomPlot from '../CustomPlot'; import { - asDynamicBytes, + asDecimal, asPercent, + asInteger, + asDynamicBytes, getFixedByteFormatter, - asDecimal, - asTime, - asInteger + asDuration } from '../../../../utils/formatters'; import { Coordinate } from '../../../../../typings/timeseries'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { useChartsSync } from '../../../../hooks/useChartsSync'; +import { Maybe } from '../../../../../typings/common'; interface Props { - start: number | string | undefined; - end: number | string | undefined; + start: Maybe; + end: Maybe; chart: GenericMetricsChart; } @@ -64,17 +65,17 @@ function getYTickFormatter(chart: GenericMetricsChart) { return getFixedByteFormatter(max); } case 'percent': { - return (y: number | null | undefined) => asPercent(y || 0, 1); + return (y: Maybe) => asPercent(y || 0, 1); } case 'time': { - return (y: number | null | undefined) => asTime(y); + return (y: Maybe) => asDuration(y); } case 'integer': { - return (y: number | null | undefined) => + return (y: Maybe) => isValidCoordinateValue(y) ? asInteger(y) : y; } default: { - return (y: number | null | undefined) => + return (y: Maybe) => isValidCoordinateValue(y) ? asDecimal(y) : y; } } @@ -89,7 +90,7 @@ function getTooltipFormatter({ yUnit }: GenericMetricsChart) { return (c: Coordinate) => asPercent(c.y || 0, 1); } case 'time': { - return (c: Coordinate) => asTime(c.y); + return (c: Coordinate) => asDuration(c.y); } case 'integer': { return (c: Coordinate) => diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js index 1f8c6db8d20a8..8ee23d61fe0eb 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js @@ -10,7 +10,7 @@ import { EuiToolTip } from '@elastic/eui'; import Legend from '../Legend'; import { units, px } from '../../../../style/variables'; import styled from 'styled-components'; -import { asTime } from '../../../../utils/formatters'; +import { asDuration } from '../../../../utils/formatters'; import theme from '@elastic/eui/dist/eui_theme_light.json'; const NameContainer = styled.div` @@ -39,7 +39,7 @@ export default function AgentMarker({ agentMark, x }) { content={
{agentMark.name} - {asTime(agentMark.us)} + {asDuration(agentMark.us)}
} > diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js index 1648f427edd7d..346aec9fb080a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js @@ -12,7 +12,7 @@ import { XYPlot, XAxis } from 'react-vis'; import LastTickValue from './LastTickValue'; import AgentMarker from './AgentMarker'; import { px } from '../../../../style/variables'; -import { getTimeFormatter } from '../../../../utils/formatters'; +import { getDurationFormatter } from '../../../../utils/formatters'; import theme from '@elastic/eui/dist/eui_theme_light.json'; // Remove any tick that is too close to topTraceDuration @@ -33,8 +33,9 @@ const getXAxisTickValues = (tickValues, topTraceDuration) => { function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues; - const tickFormat = getTimeFormatter(xMax); + const tickFormatter = getDurationFormatter(xMax); const xAxisTickValues = getXAxisTickValues(tickValues, topTraceDuration); + const topTraceDurationFormatted = tickFormatter(topTraceDuration).formatted; return ( @@ -66,7 +67,7 @@ function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { orientation="top" tickSize={0} tickValues={xAxisTickValues} - tickFormat={tickFormat} + tickFormat={time => tickFormatter(time).formatted} tickPadding={20} style={{ text: { fill: theme.euiColorDarkShade } @@ -76,7 +77,7 @@ function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { {topTraceDuration > 0 && ( )} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js index f5992ac7fc63b..239e46c25904d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js @@ -19,7 +19,7 @@ import { } from '../../../../style/variables'; import Legend from '../Legend'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { asAbsoluteTime } from '../../TimestampTooltip'; +import { asAbsoluteDateTime } from '../../../../utils/formatters'; const TooltipElm = styled.div` margin: 0 ${px(unit)}; @@ -87,9 +87,7 @@ export default function Tooltip({ return ( -
- {header || asAbsoluteTime({ time: x, precision: 'seconds' })} -
+
{header || asAbsoluteDateTime(x, 'seconds')}
{showLegends ? ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx index adcce161c7ac1..d2b6970841bdc 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { asTime, asInteger } from '../../../../../utils/formatters'; +import { asDuration, asInteger } from '../../../../../utils/formatters'; import { fontSizes } from '../../../../../style/variables'; export const ChoroplethToolTip: React.SFC<{ @@ -26,7 +26,7 @@ export const ChoroplethToolTip: React.SFC<{ )}
- {asTime(value)} + {asDuration(value)}
( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 94f30a8a2325a..b5894a9d91e4a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -27,13 +27,13 @@ import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { asInteger, tpmUnit, - TimeFormatter + TimeFormatter, + getDurationFormatter } from '../../../../utils/formatters'; import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; import { LicenseContext } from '../../../../context/LicenseContext'; import { TransactionLineChart } from './TransactionLineChart'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { getTimeFormatter } from '../../../../utils/formatters'; import { DurationByCountryMap } from './DurationByCountryMap'; import { TRANSACTION_PAGE_LOAD, @@ -74,12 +74,14 @@ export class TransactionCharts extends Component { }; public getResponseTimeTickFormatter = (formatter: TimeFormatter) => { - return (t: number) => formatter(t); + return (t: number) => formatter(t).formatted; }; public getResponseTimeTooltipFormatter = (formatter: TimeFormatter) => { return (p: Coordinate) => { - return isValidCoordinateValue(p.y) ? formatter(p.y) : NOT_AVAILABLE_LABEL; + return isValidCoordinateValue(p.y) + ? formatter(p.y).formatted + : NOT_AVAILABLE_LABEL; }; }; @@ -154,7 +156,7 @@ export class TransactionCharts extends Component { const { responseTimeSeries, tpmSeries } = charts; const { transactionType } = urlParams; const maxY = this.getMaxY(responseTimeSeries); - const formatter = getTimeFormatter(maxY); + const formatter = getDurationFormatter(maxY); return ( <> diff --git a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts index b15231e89365a..75a558ac81a54 100644 --- a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts +++ b/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts @@ -16,7 +16,7 @@ import { RectCoordinate, TimeSeries } from '../../typings/timeseries'; -import { asDecimal, asMillis, tpmUnit } from '../utils/formatters'; +import { asDecimal, tpmUnit, convertTo } from '../utils/formatters'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries'; import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor'; @@ -70,6 +70,10 @@ export function getResponseTimeSeries({ }: TimeSeriesAPIResponse) { const { overallAvgDuration } = apmTimeseries; const { avg, p95, p99 } = apmTimeseries.responseTimes; + const formattedDuration = convertTo({ + unit: 'milliseconds', + microseconds: overallAvgDuration + }).formatted; const series: TimeSeries[] = [ { @@ -77,7 +81,7 @@ export function getResponseTimeSeries({ defaultMessage: 'Avg.' }), data: avg, - legendValue: asMillis(overallAvgDuration), + legendValue: formattedDuration, type: 'linemark', color: theme.euiColorVis1 }, diff --git a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts b/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts index 01a58ac03d0c3..295ea1f9f900f 100644 --- a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts +++ b/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts @@ -5,6 +5,7 @@ */ import { compact, isObject } from 'lodash'; +import { Maybe } from '../../typings/common'; export interface KeyValuePair { key: string; @@ -12,7 +13,7 @@ export interface KeyValuePair { } export const flattenObject = ( - item: Record | null | undefined, + item: Maybe>, parentKey?: string ): KeyValuePair[] => { if (item) { diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters.ts b/x-pack/legacy/plugins/apm/public/utils/formatters.ts deleted file mode 100644 index 34b552230fa77..0000000000000 --- a/x-pack/legacy/plugins/apm/public/utils/formatters.ts +++ /dev/null @@ -1,247 +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 numeral from '@elastic/numeral'; -import { i18n } from '@kbn/i18n'; -import { memoize } from 'lodash'; -import { NOT_AVAILABLE_LABEL } from '../../common/i18n'; - -const HOURS_CUT_OFF = 3600000000; // 1 hour (in microseconds) -const MINUTES_CUT_OFF = 60000000; // 1 minute (in microseconds) -const SECONDS_CUT_OFF = 10 * 1000000; // 10 seconds (in microseconds) -const MILLISECONDS_CUT_OFF = 10 * 1000; // 10 milliseconds (in microseconds) -const SPACE = ' '; - -/* - * value: time in microseconds - * withUnit: add unit suffix - * defaultValue: value to use if the specified is null/undefined - */ -type FormatterValue = number | undefined | null; -interface FormatterOptions { - withUnit?: boolean; - defaultValue?: string; -} - -export function asHours( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const hoursLabel = - SPACE + - i18n.translate('xpack.apm.formatters.hoursTimeUnitLabel', { - defaultMessage: 'h' - }); - const formatted = asDecimal(value / 3600000000); - return `${formatted}${withUnit ? hoursLabel : ''}`; -} - -export function asMinutes( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const minutesLabel = - SPACE + - i18n.translate('xpack.apm.formatters.minutesTimeUnitLabel', { - defaultMessage: 'min' - }); - const formatted = asDecimal(value / 60000000); - return `${formatted}${withUnit ? minutesLabel : ''}`; -} - -export function asSeconds( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const secondsLabel = - SPACE + - i18n.translate('xpack.apm.formatters.secondsTimeUnitLabel', { - defaultMessage: 's' - }); - const formatted = asDecimal(value / 1000000); - return `${formatted}${withUnit ? secondsLabel : ''}`; -} - -export function asMillis( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - - const millisLabel = - SPACE + - i18n.translate('xpack.apm.formatters.millisTimeUnitLabel', { - defaultMessage: 'ms' - }); - const formatted = asInteger(value / 1000); - return `${formatted}${withUnit ? millisLabel : ''}`; -} - -export function asMicros( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - - const microsLabel = - SPACE + - i18n.translate('xpack.apm.formatters.microsTimeUnitLabel', { - defaultMessage: 'μs' - }); - const formatted = asInteger(value); - return `${formatted}${withUnit ? microsLabel : ''}`; -} - -export type TimeFormatter = ( - value: FormatterValue, - options?: FormatterOptions -) => string; - -type TimeFormatterBuilder = (max: number) => TimeFormatter; - -export const getTimeFormatter: TimeFormatterBuilder = memoize((max: number) => { - const unit = timeUnit(max); - switch (unit) { - case 'h': - return asHours; - case 'm': - return asMinutes; - case 's': - return asSeconds; - case 'ms': - return asMillis; - case 'us': - return asMicros; - } -}); - -export function timeUnit(max: number) { - if (max > HOURS_CUT_OFF) { - return 'h'; - } else if (max > MINUTES_CUT_OFF) { - return 'm'; - } else if (max > SECONDS_CUT_OFF) { - return 's'; - } else if (max > MILLISECONDS_CUT_OFF) { - return 'ms'; - } else { - return 'us'; - } -} - -export function asTime( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const formatter = getTimeFormatter(value); - return formatter(value, { withUnit, defaultValue }); -} - -export function asDecimal(value: number) { - return numeral(value).format('0,0.0'); -} - -export function asInteger(value: number) { - return numeral(value).format('0,0'); -} - -export function tpmUnit(type?: string) { - return type === 'request' - ? i18n.translate('xpack.apm.formatters.requestsPerMinLabel', { - defaultMessage: 'rpm' - }) - : i18n.translate('xpack.apm.formatters.transactionsPerMinLabel', { - defaultMessage: 'tpm' - }); -} - -export function asPercent( - numerator: number, - denominator: number | undefined, - fallbackResult = '' -) { - if (!denominator || isNaN(numerator)) { - return fallbackResult; - } - - const decimal = numerator / denominator; - return numeral(decimal).format('0.0%'); -} - -function asKilobytes(value: number) { - return `${asDecimal(value / 1000)} KB`; -} - -function asMegabytes(value: number) { - return `${asDecimal(value / 1e6)} MB`; -} - -function asGigabytes(value: number) { - return `${asDecimal(value / 1e9)} GB`; -} - -function asTerabytes(value: number) { - return `${asDecimal(value / 1e12)} TB`; -} - -function asBytes(value: number) { - return `${asDecimal(value)} B`; -} - -const bailIfNumberInvalid = (cb: (val: number) => string) => { - return (val: number | null | undefined) => { - if (val === null || val === undefined || isNaN(val)) { - return ''; - } - return cb(val); - }; -}; - -export const asDynamicBytes = bailIfNumberInvalid((value: number) => { - return unmemoizedFixedByteFormatter(value)(value); -}); - -const unmemoizedFixedByteFormatter = (max: number) => { - if (max > 1e12) { - return asTerabytes; - } - - if (max > 1e9) { - return asGigabytes; - } - - if (max > 1e6) { - return asMegabytes; - } - - if (max > 1000) { - return asKilobytes; - } - - return asBytes; -}; - -export const getFixedByteFormatter = memoize((max: number) => { - const formatter = unmemoizedFixedByteFormatter(max); - - return bailIfNumberInvalid(formatter); -}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts new file mode 100644 index 0000000000000..bec9cede00a2b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts @@ -0,0 +1,146 @@ +/* + * 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 moment from 'moment-timezone'; +import { asRelativeDateTimeRange, asAbsoluteDateTime } from '../datetime'; + +describe('date time formatters', () => { + describe('asRelativeDateTimeRange', () => { + beforeAll(() => { + moment.tz.setDefault('Europe/Amsterdam'); + }); + afterAll(() => moment.tz.setDefault('')); + const formatDateToTimezone = (dateTimeString: string) => + moment(dateTimeString).valueOf(); + + describe('YYYY - YYYY', () => { + it('range: 10 years', () => { + const start = formatDateToTimezone('2000-01-01 10:01:01'); + const end = formatDateToTimezone('2010-01-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('2000 - 2010'); + }); + it('range: 5 years', () => { + const start = formatDateToTimezone('2010-01-01 10:01:01'); + const end = formatDateToTimezone('2015-01-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('2010 - 2015'); + }); + }); + describe('MMM YYYY - MMM YYYY', () => { + it('range: 4 years ', () => { + const start = formatDateToTimezone('2010-01-01 10:01:01'); + const end = formatDateToTimezone('2014-04-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Jan 2010 - Apr 2014'); + }); + it('range: 6 months ', () => { + const start = formatDateToTimezone('2019-01-01 10:01:01'); + const end = formatDateToTimezone('2019-07-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Jan 2019 - Jul 2019'); + }); + }); + describe('MMM D, YYYY - MMM D, YYYY', () => { + it('range: 2 days', () => { + const start = formatDateToTimezone('2019-10-01 10:01:01'); + const end = formatDateToTimezone('2019-10-05 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 1, 2019 - Oct 5, 2019'); + }); + it('range: 1 day', () => { + const start = formatDateToTimezone('2019-10-01 10:01:01'); + const end = formatDateToTimezone('2019-10-03 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 1, 2019 - Oct 3, 2019'); + }); + }); + describe('MMM D, YYYY, HH:mm - HH:mm (UTC)', () => { + it('range: 9 hours', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 19:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01 - 19:01 (UTC+1)'); + }); + it('range: 5 hours', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 15:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01 - 15:01 (UTC+1)'); + }); + }); + describe('MMM D, YYYY, HH:mm:ss - HH:mm:ss (UTC)', () => { + it('range: 14 minutes', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 10:15:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01:01 - 10:15:01 (UTC+1)'); + }); + it('range: 5 minutes', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 10:06:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01:01 - 10:06:01 (UTC+1)'); + }); + }); + describe('MMM D, YYYY, HH:mm:ss.SSS - HH:mm:ss.SSS (UTC)', () => { + it('range: 9 seconds', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01.001'); + const end = formatDateToTimezone('2019-10-29 10:01:10.002'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual( + 'Oct 29, 2019, 10:01:01.001 - 10:01:10.002 (UTC+1)' + ); + }); + it('range: 1 second', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01.001'); + const end = formatDateToTimezone('2019-10-29 10:01:02.002'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual( + 'Oct 29, 2019, 10:01:01.001 - 10:01:02.002 (UTC+1)' + ); + }); + }); + }); + + describe('asAbsoluteDateTime', () => { + afterAll(() => moment.tz.setDefault('')); + + it('should add a leading plus for timezones with positive UTC offset', () => { + moment.tz.setDefault('Europe/Copenhagen'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 14:00 (UTC+2)' + ); + }); + + it('should add a leading minus for timezones with negative UTC offset', () => { + moment.tz.setDefault('America/Los_Angeles'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 05:00 (UTC-7)' + ); + }); + + it('should use default UTC offset formatting when offset contains minutes', () => { + moment.tz.setDefault('Canada/Newfoundland'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 09:30 (UTC-02:30)' + ); + }); + + it('should respect DST', () => { + moment.tz.setDefault('Europe/Copenhagen'); + const timeWithDST = 1559390400000; // Jun 1, 2019 + const timeWithoutDST = 1575201600000; // Dec 1, 2019 + + expect(asAbsoluteDateTime(timeWithDST)).toBe( + 'Jun 1, 2019, 14:00:00.000 (UTC+2)' + ); + + expect(asAbsoluteDateTime(timeWithoutDST)).toBe( + 'Dec 1, 2019, 13:00:00.000 (UTC+1)' + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts new file mode 100644 index 0000000000000..014ecad01d4d7 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { asDuration, convertTo, toMicroseconds } from '../duration'; + +describe('duration formatters', () => { + describe('asDuration', () => { + it('formats correctly with defaults', () => { + expect(asDuration(null)).toEqual('N/A'); + expect(asDuration(undefined)).toEqual('N/A'); + expect(asDuration(0)).toEqual('0 μs'); + expect(asDuration(1)).toEqual('1 μs'); + expect(asDuration(toMicroseconds(1, 'milliseconds'))).toEqual('1,000 μs'); + expect(asDuration(toMicroseconds(1000, 'milliseconds'))).toEqual( + '1,000 ms' + ); + expect(asDuration(toMicroseconds(10000, 'milliseconds'))).toEqual( + '10,000 ms' + ); + expect(asDuration(toMicroseconds(20, 'seconds'))).toEqual('20.0 s'); + expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('10.0 min'); + expect(asDuration(toMicroseconds(1, 'hours'))).toEqual('60.0 min'); + expect(asDuration(toMicroseconds(1.5, 'hours'))).toEqual('1.5 h'); + }); + + it('falls back to default value', () => { + expect(asDuration(undefined, { defaultValue: 'nope' })).toEqual('nope'); + }); + }); + + describe('convertTo', () => { + it('hours', () => { + const unit = 'hours'; + const oneHourAsMicro = toMicroseconds(1, 'hours'); + const twoHourAsMicro = toMicroseconds(2, 'hours'); + expect(convertTo({ unit, microseconds: oneHourAsMicro })).toEqual({ + unit: 'h', + value: '1.0', + formatted: '1.0 h' + }); + expect(convertTo({ unit, microseconds: twoHourAsMicro })).toEqual({ + unit: 'h', + value: '2.0', + formatted: '2.0 h' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '1.2' }) + ).toEqual({ value: '1.2', formatted: '1.2' }); + }); + + it('minutes', () => { + const unit = 'minutes'; + const oneHourAsMicro = toMicroseconds(1, 'hours'); + const twoHourAsMicro = toMicroseconds(2, 'hours'); + expect(convertTo({ unit, microseconds: oneHourAsMicro })).toEqual({ + unit: 'min', + value: '60.0', + formatted: '60.0 min' + }); + expect(convertTo({ unit, microseconds: twoHourAsMicro })).toEqual({ + unit: 'min', + value: '120.0', + formatted: '120.0 min' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('seconds', () => { + const unit = 'seconds'; + const twentySecondsAsMicro = toMicroseconds(20, 'seconds'); + const thirtyFiveSecondsAsMicro = toMicroseconds(35, 'seconds'); + expect(convertTo({ unit, microseconds: twentySecondsAsMicro })).toEqual({ + unit: 's', + value: '20.0', + formatted: '20.0 s' + }); + expect( + convertTo({ unit, microseconds: thirtyFiveSecondsAsMicro }) + ).toEqual({ unit: 's', value: '35.0', formatted: '35.0 s' }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('milliseconds', () => { + const unit = 'milliseconds'; + const twentyMilliAsMicro = toMicroseconds(20, 'milliseconds'); + const thirtyFiveMilliAsMicro = toMicroseconds(35, 'milliseconds'); + expect(convertTo({ unit, microseconds: twentyMilliAsMicro })).toEqual({ + unit: 'ms', + value: '20', + formatted: '20 ms' + }); + expect( + convertTo({ unit, microseconds: thirtyFiveMilliAsMicro }) + ).toEqual({ unit: 'ms', value: '35', formatted: '35 ms' }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('microseconds', () => { + const unit = 'microseconds'; + expect(convertTo({ unit, microseconds: 20 })).toEqual({ + unit: 'μs', + value: '20', + formatted: '20 μs' + }); + expect(convertTo({ unit, microseconds: 35 })).toEqual({ + unit: 'μs', + value: '35', + formatted: '35 μs' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + }); + describe('toMicroseconds', () => { + it('transformes to microseconds', () => { + expect(toMicroseconds(1, 'hours')).toEqual(3600000000); + expect(toMicroseconds(10, 'minutes')).toEqual(600000000); + expect(toMicroseconds(10, 'seconds')).toEqual(10000000); + expect(toMicroseconds(10, 'milliseconds')).toEqual(10000); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts new file mode 100644 index 0000000000000..f6ed88a850a5b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { asPercent } from '../formatters'; + +describe('formatters', () => { + describe('asPercent', () => { + it('should divide and format item as percent', () => { + expect(asPercent(3725, 10000, 'n/a')).toEqual('37.3%'); + }); + + it('should format when numerator is 0', () => { + expect(asPercent(0, 1, 'n/a')).toEqual('0.0%'); + }); + + it('should return fallback when denominator is undefined', () => { + expect(asPercent(3725, undefined, 'n/a')).toEqual('n/a'); + }); + + it('should return fallback when denominator is 0 ', () => { + expect(asPercent(3725, 0, 'n/a')).toEqual('n/a'); + }); + + it('should return fallback when numerator or denominator is NaN', () => { + expect(asPercent(3725, NaN, 'n/a')).toEqual('n/a'); + expect(asPercent(NaN, 10000, 'n/a')).toEqual('n/a'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts similarity index 63% rename from x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts rename to x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts index 093624240565f..07d3d0c1eb08f 100644 --- a/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts @@ -3,61 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { getFixedByteFormatter, asDynamicBytes } from '../size'; -import { - asPercent, - asTime, - getFixedByteFormatter, - asDynamicBytes -} from '../formatters'; - -describe('formatters', () => { - describe('asTime', () => { - it('formats correctly with defaults', () => { - expect(asTime(null)).toEqual('N/A'); - expect(asTime(undefined)).toEqual('N/A'); - expect(asTime(0)).toEqual('0 μs'); - expect(asTime(1)).toEqual('1 μs'); - expect(asTime(1000)).toEqual('1,000 μs'); - expect(asTime(1000 * 1000)).toEqual('1,000 ms'); - expect(asTime(1000 * 1000 * 10)).toEqual('10,000 ms'); - expect(asTime(1000 * 1000 * 20)).toEqual('20.0 s'); - expect(asTime(60000000 * 10)).toEqual('10.0 min'); - expect(asTime(3600000000 * 1.5)).toEqual('1.5 h'); - }); - - it('formats without unit', () => { - expect(asTime(1000, { withUnit: false })).toEqual('1,000'); - }); - - it('falls back to default value', () => { - expect(asTime(undefined, { defaultValue: 'nope' })).toEqual('nope'); - }); - }); - - describe('asPercent', () => { - it('should divide and format item as percent', () => { - expect(asPercent(3725, 10000, 'n/a')).toEqual('37.3%'); - }); - - it('should format when numerator is 0', () => { - expect(asPercent(0, 1, 'n/a')).toEqual('0.0%'); - }); - - it('should return fallback when denominator is undefined', () => { - expect(asPercent(3725, undefined, 'n/a')).toEqual('n/a'); - }); - - it('should return fallback when denominator is 0 ', () => { - expect(asPercent(3725, 0, 'n/a')).toEqual('n/a'); - }); - - it('should return fallback when numerator or denominator is NaN', () => { - expect(asPercent(3725, NaN, 'n/a')).toEqual('n/a'); - expect(asPercent(NaN, 10000, 'n/a')).toEqual('n/a'); - }); - }); - +describe('size formatters', () => { describe('byte formatting', () => { const bytes = 10; const kb = 1000 + 1; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts new file mode 100644 index 0000000000000..98483a0351f06 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts @@ -0,0 +1,149 @@ +/* + * 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 moment from 'moment-timezone'; + +/** + * Returns the timezone set on momentTime. + * (UTC+offset) when offset if bigger than 0. + * (UTC-offset) when offset if lower than 0. + * @param momentTime Moment + */ +function formatTimezone(momentTime: moment.Moment) { + const DEFAULT_TIMEZONE_FORMAT = 'Z'; + + const utcOffsetHours = momentTime.utcOffset() / 60; + + const customTimezoneFormat = + utcOffsetHours > 0 ? `+${utcOffsetHours}` : utcOffsetHours; + + const utcOffsetFormatted = Number.isInteger(utcOffsetHours) + ? customTimezoneFormat + : DEFAULT_TIMEZONE_FORMAT; + + return momentTime.format(`(UTC${utcOffsetFormatted})`); +} + +export type TimeUnit = 'hours' | 'minutes' | 'seconds' | 'milliseconds'; +function getTimeFormat(timeUnit: TimeUnit) { + switch (timeUnit) { + case 'hours': + return 'HH'; + case 'minutes': + return 'HH:mm'; + case 'seconds': + return 'HH:mm:ss'; + case 'milliseconds': + return 'HH:mm:ss.SSS'; + default: + return ''; + } +} + +type DateUnit = 'days' | 'months' | 'years'; +function getDateFormat(dateUnit: DateUnit) { + switch (dateUnit) { + case 'years': + return 'YYYY'; + case 'months': + return 'MMM YYYY'; + case 'days': + return 'MMM D, YYYY'; + default: + return ''; + } +} + +function getFormatsAccordingToDateDifference( + momentStart: moment.Moment, + momentEnd: moment.Moment +) { + const getDateDifference = (unitOfTime: DateUnit | TimeUnit) => + momentEnd.diff(momentStart, unitOfTime); + + if (getDateDifference('years') >= 5) { + return { dateFormat: getDateFormat('years') }; + } + + if (getDateDifference('months') >= 5) { + return { dateFormat: getDateFormat('months') }; + } + + const dateFormatWithDays = getDateFormat('days'); + if (getDateDifference('days') > 1) { + return { dateFormat: dateFormatWithDays }; + } + + if (getDateDifference('hours') >= 5) { + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('minutes') + }; + } + + if (getDateDifference('minutes') >= 5) { + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('seconds') + }; + } + + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('milliseconds') + }; +} + +export function asAbsoluteDateTime( + time: number, + timeUnit: TimeUnit = 'milliseconds' +) { + const momentTime = moment(time); + const formattedTz = formatTimezone(momentTime); + + return momentTime.format( + `${getDateFormat('days')}, ${getTimeFormat(timeUnit)} ${formattedTz}` + ); +} + +/** + * + * Returns the dates formatted according to the difference between the two dates: + * + * | Difference | Format | + * | -------------- |:----------------------------------------------:| + * | >= 5 years | YYYY - YYYY | + * | >= 5 months | MMM YYYY - MMM YYYY | + * | > 1 day | MMM D, YYYY - MMM D, YYYY | + * | >= 5 hours | MMM D, YYYY, HH:mm - HH:mm (UTC) | + * | >= 5 minutes | MMM D, YYYY, HH:mm:ss - HH:mm:ss (UTC) | + * | default | MMM D, YYYY, HH:mm:ss.SSS - HH:mm:ss.SSS (UTC) | + * + * @param start timestamp + * @param end timestamp + */ +export function asRelativeDateTimeRange(start: number, end: number) { + const momentStartTime = moment(start); + const momentEndTime = moment(end); + + const { dateFormat, timeFormat } = getFormatsAccordingToDateDifference( + momentStartTime, + momentEndTime + ); + + if (timeFormat) { + const startFormatted = momentStartTime.format( + `${dateFormat}, ${timeFormat}` + ); + const endFormatted = momentEndTime.format(timeFormat); + const formattedTz = formatTimezone(momentStartTime); + return `${startFormatted} - ${endFormatted} ${formattedTz}`; + } + + const startFormatted = momentStartTime.format(dateFormat); + const endFormatted = momentEndTime.format(dateFormat); + return `${startFormatted} - ${endFormatted}`; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts new file mode 100644 index 0000000000000..39341e1ff4443 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts @@ -0,0 +1,153 @@ +/* + * 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'; +import moment from 'moment'; +import { memoize } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; +import { asDecimal, asInteger } from './formatters'; +import { TimeUnit } from './datetime'; +import { Maybe } from '../../../typings/common'; + +interface FormatterOptions { + defaultValue?: string; +} + +type DurationTimeUnit = TimeUnit | 'microseconds'; + +interface DurationUnit { + [unit: string]: { + label: string; + convert: (value: number) => string; + }; +} + +interface ConvertedDuration { + value: string; + unit?: string; + formatted: string; +} + +export type TimeFormatter = ( + value: Maybe, + options?: FormatterOptions +) => ConvertedDuration; + +type TimeFormatterBuilder = (max: number) => TimeFormatter; + +const durationUnit: DurationUnit = { + hours: { + label: i18n.translate('xpack.apm.formatters.hoursTimeUnitLabel', { + defaultMessage: 'h' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asHours()) + }, + minutes: { + label: i18n.translate('xpack.apm.formatters.minutesTimeUnitLabel', { + defaultMessage: 'min' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asMinutes()) + }, + seconds: { + label: i18n.translate('xpack.apm.formatters.secondsTimeUnitLabel', { + defaultMessage: 's' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asSeconds()) + }, + milliseconds: { + label: i18n.translate('xpack.apm.formatters.millisTimeUnitLabel', { + defaultMessage: 'ms' + }), + convert: (value: number) => + asInteger(moment.duration(value / 1000).asMilliseconds()) + }, + microseconds: { + label: i18n.translate('xpack.apm.formatters.microsTimeUnitLabel', { + defaultMessage: 'μs' + }), + convert: (value: number) => asInteger(value) + } +}; + +/** + * Converts a microseconds value into the unit defined. + * + * @param param0 + * { unit: "milliseconds" | "hours" | "minutes" | "seconds" | "microseconds", microseconds, defaultValue } + * + * @returns object { value, unit, formatted } + */ +export function convertTo({ + unit, + microseconds, + defaultValue = NOT_AVAILABLE_LABEL +}: { + unit: DurationTimeUnit; + microseconds: Maybe; + defaultValue?: string; +}): ConvertedDuration { + const duration = durationUnit[unit]; + if (!duration || microseconds == null) { + return { value: defaultValue, formatted: defaultValue }; + } + + const convertedValue = duration.convert(microseconds); + return { + value: convertedValue, + unit: duration.label, + formatted: `${convertedValue} ${duration.label}` + }; +} + +export const toMicroseconds = (value: number, timeUnit: TimeUnit) => + moment.duration(value, timeUnit).asMilliseconds() * 1000; + +function getDurationUnitKey(max: number): DurationTimeUnit { + if (max > toMicroseconds(1, 'hours')) { + return 'hours'; + } + if (max > toMicroseconds(1, 'minutes')) { + return 'minutes'; + } + if (max > toMicroseconds(10, 'seconds')) { + return 'seconds'; + } + if (max > toMicroseconds(10, 'milliseconds')) { + return 'milliseconds'; + } + return 'microseconds'; +} + +export const getDurationFormatter: TimeFormatterBuilder = memoize( + (max: number) => { + const unit = getDurationUnitKey(max); + return (value, { defaultValue }: FormatterOptions = {}) => { + return convertTo({ unit, microseconds: value, defaultValue }); + }; + } +); + +/** + * Converts value and returns it formatted - 00 unit + * + * @param value + * @param param1 { defaultValue } + * @returns formated value - 00 unit + */ +export function asDuration( + value: Maybe, + { defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} +) { + if (value == null) { + return defaultValue; + } + + const formatter = getDurationFormatter(value); + return formatter(value, { defaultValue }).formatted; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts new file mode 100644 index 0000000000000..630b6a0a18dbf --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; + +export function asDecimal(value: number) { + return numeral(value).format('0,0.0'); +} + +export function asInteger(value: number) { + return numeral(value).format('0,0'); +} + +export function tpmUnit(type?: string) { + return type === 'request' + ? i18n.translate('xpack.apm.formatters.requestsPerMinLabel', { + defaultMessage: 'rpm' + }) + : i18n.translate('xpack.apm.formatters.transactionsPerMinLabel', { + defaultMessage: 'tpm' + }); +} + +export function asPercent( + numerator: number, + denominator: number | undefined, + fallbackResult = '' +) { + if (!denominator || isNaN(numerator)) { + return fallbackResult; + } + + const decimal = numerator / denominator; + return numeral(decimal).format('0.0%'); +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts new file mode 100644 index 0000000000000..4fedd55ff1e89 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './formatters'; +export * from './datetime'; +export * from './duration'; +export * from './size'; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts new file mode 100644 index 0000000000000..2cdf8af1d46de --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts @@ -0,0 +1,67 @@ +/* + * 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 { memoize } from 'lodash'; +import { asDecimal } from './formatters'; +import { Maybe } from '../../../typings/common'; + +function asKilobytes(value: number) { + return `${asDecimal(value / 1000)} KB`; +} + +function asMegabytes(value: number) { + return `${asDecimal(value / 1e6)} MB`; +} + +function asGigabytes(value: number) { + return `${asDecimal(value / 1e9)} GB`; +} + +function asTerabytes(value: number) { + return `${asDecimal(value / 1e12)} TB`; +} + +function asBytes(value: number) { + return `${asDecimal(value)} B`; +} + +const bailIfNumberInvalid = (cb: (val: number) => string) => { + return (val: Maybe) => { + if (val === null || val === undefined || isNaN(val)) { + return ''; + } + return cb(val); + }; +}; + +export const getFixedByteFormatter = memoize((max: number) => { + const formatter = unmemoizedFixedByteFormatter(max); + + return bailIfNumberInvalid(formatter); +}); + +export const asDynamicBytes = bailIfNumberInvalid((value: number) => { + return unmemoizedFixedByteFormatter(value)(value); +}); + +const unmemoizedFixedByteFormatter = (max: number) => { + if (max > 1e12) { + return asTerabytes; + } + + if (max > 1e9) { + return asGigabytes; + } + + if (max > 1e6) { + return asMegabytes; + } + + if (max > 1000) { + return asKilobytes; + } + + return asBytes; +}; diff --git a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts b/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts index 411d03fce349d..c36efc232b782 100644 --- a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts +++ b/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Maybe } from '../../typings/common'; -export const isValidCoordinateValue = ( - value: number | null | undefined -): value is number => value !== null && value !== undefined; +export const isValidCoordinateValue = (value: Maybe): value is number => + value !== null && value !== undefined; diff --git a/x-pack/legacy/plugins/apm/typings/common.d.ts b/x-pack/legacy/plugins/apm/typings/common.d.ts index d79b05ed99b49..b9064980bd657 100644 --- a/x-pack/legacy/plugins/apm/typings/common.d.ts +++ b/x-pack/legacy/plugins/apm/typings/common.d.ts @@ -27,3 +27,5 @@ export type PromiseReturnType = Func extends ( ) => Promise ? Value : Func; + +export type Maybe = T | null | undefined; diff --git a/x-pack/legacy/plugins/apm/typings/timeseries.ts b/x-pack/legacy/plugins/apm/typings/timeseries.ts index 9b9f7dcc2c820..d64486d8e71e9 100644 --- a/x-pack/legacy/plugins/apm/typings/timeseries.ts +++ b/x-pack/legacy/plugins/apm/typings/timeseries.ts @@ -3,10 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Maybe } from '../typings/common'; export interface Coordinate { x: number; - y: number | null | undefined; + y: Maybe; } export interface RectCoordinate { From e064205471590fe10c06feefda0b5caae4de2109 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Fri, 15 Nov 2019 13:56:56 +0100 Subject: [PATCH 12/44] update chromedriver to 78 (#50737) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c0e2acdb578c4..3abd69a616682 100644 --- a/package.json +++ b/package.json @@ -360,7 +360,7 @@ "chance": "1.0.18", "cheerio": "0.22.0", "chokidar": "3.2.1", - "chromedriver": "^77.0.0", + "chromedriver": "78.0.1", "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 5bb6bb3da2e47..63726faf0adb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7737,10 +7737,10 @@ chrome-trace-event@^1.0.0, chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^77.0.0: - version "77.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-77.0.0.tgz#bd916cc87a0ccb7a6e4fb4b43cb2368bc54db6a0" - integrity sha512-mZa1IVx4HD8rDaItWbnS470mmypgiWsDiu98r0NkiT4uLm3qrANl4vOU6no6vtWtLQiW5kt1POcIbjeNpsLbXA== +chromedriver@78.0.1: + version "78.0.1" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-78.0.1.tgz#2db3425a2cba6fcaf1a41d9538b16c3d06fa74a8" + integrity sha512-eOsyFk4xb9EECs1VMrDbxO713qN+Bu1XUE8K9AuePc3839TPdAegg72kpXSzkeNqRNZiHbnJUItIVCLFkDqceA== dependencies: del "^4.1.1" extract-zip "^1.6.7" From 63a70c0d2ad9d6d6dbf169be629f4c83b3b906fc Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 15 Nov 2019 15:19:02 +0100 Subject: [PATCH 13/44] [Grokdebugger] Theme and Mode imports (#50473) * Added textmate theme and JSON mode imports * Added text themes to other inputs [skip ci] --- .../custom_patterns_input/custom_patterns_input.js | 2 ++ .../public/components/event_input/event_input.js | 2 ++ .../public/components/event_output/event_output.js | 1 + .../public/components/grok_debugger/brace_imports.ts | 9 +++++++++ .../public/components/grok_debugger/grok_debugger.js | 3 +++ .../public/components/pattern_input/pattern_input.js | 1 + 6 files changed, 18 insertions(+) create mode 100644 x-pack/legacy/plugins/grokdebugger/public/components/grok_debugger/brace_imports.ts diff --git a/x-pack/legacy/plugins/grokdebugger/public/components/custom_patterns_input/custom_patterns_input.js b/x-pack/legacy/plugins/grokdebugger/public/components/custom_patterns_input/custom_patterns_input.js index cc1d2bc53a9df..7f0b25e7e25af 100644 --- a/x-pack/legacy/plugins/grokdebugger/public/components/custom_patterns_input/custom_patterns_input.js +++ b/x-pack/legacy/plugins/grokdebugger/public/components/custom_patterns_input/custom_patterns_input.js @@ -57,6 +57,8 @@ MSG message-id=<%{GREEDYDATA}>`; Date: Fri, 15 Nov 2019 15:15:13 +0000 Subject: [PATCH 14/44] Adding tooltip explanation on range filters (#50186) --- .../vis/editors/default/controls/ranges.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx b/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx index 071e15f8b97f8..a216ad5d928b6 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx @@ -29,6 +29,8 @@ import { EuiSpacer, EuiButtonEmpty, EuiFormRow, + EuiToolTip, + EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -154,15 +156,24 @@ function RangesParamEditor({ [isFromValid, isToValid] = validateRange({ from, to }, index); } - const fromPrepend = i18n.translate( + const gtePrependLabel = i18n.translate( 'common.ui.aggTypes.ranges.greaterThanOrEqualPrepend', { defaultMessage: '\u2265', } ); - const toPrepend = i18n.translate('common.ui.aggTypes.ranges.lessThanPrepend', { + const gteTooltipContent = i18n.translate( + 'common.ui.aggTypes.ranges.greaterThanOrEqualTooltip', + { + defaultMessage: 'Greater than or equal to', + } + ); + const ltPrependLabel = i18n.translate('common.ui.aggTypes.ranges.lessThanPrepend', { defaultMessage: '\u003c', }); + const ltTooltipContent = i18n.translate('common.ui.aggTypes.ranges.lessThanTooltip', { + defaultMessage: 'Less than', + }); return ( @@ -179,7 +190,11 @@ function RangesParamEditor({ fullWidth={true} compressed={true} isInvalid={!isFromValid} - prepend={fromPrepend} + prepend={ + + {gtePrependLabel} + + } /> @@ -197,7 +212,11 @@ function RangesParamEditor({ fullWidth={true} compressed={true} isInvalid={!isToValid} - prepend={toPrepend} + prepend={ + + {ltPrependLabel} + + } /> From ae413abfa0428092ff259125b68dbb0ffaa0873a Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 13 Nov 2019 16:13:49 -0800 Subject: [PATCH 15/44] Skip flaky " can navigate Autoplay Settings, closes" test Signed-off-by: Tyler Smalley --- .../components/footer/settings/__tests__/settings.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx index 6957e8599c0fd..d5963a936fa70 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx @@ -71,7 +71,7 @@ describe('', () => { expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot(); }); - test('can navigate Toolbar Settings, closes when activated', async () => { + test.skip('can navigate Toolbar Settings, closes when activated', async () => { trigger(wrapper).simulate('click'); expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot(); menuItems(wrapper) From 1dfc70fe6828532337a1fef15cae2c4a90603c7b Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 13 Nov 2019 14:25:14 -0800 Subject: [PATCH 16/44] Skip flaky " can navigate Autoplay Settings" test Signed-off-by: Tyler Smalley --- .../components/footer/settings/__tests__/settings.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx index d5963a936fa70..0667674b6a7dd 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx @@ -60,7 +60,7 @@ describe('', () => { expect(popover(wrapper).prop('isOpen')).toEqual(false); }); - test('can navigate Autoplay Settings', async () => { + test.skip('can navigate Autoplay Settings', async () => { trigger(wrapper).simulate('click'); expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot(); await tick(20); From 1c415e0cadd46a871de12ca42c066f23c44ea128 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 15 Nov 2019 10:53:33 -0500 Subject: [PATCH 17/44] Default payload validation (#48753) * trial for default payload validation * relaxing default validation * some cleanup and testing * update xsrf integration test * adding API smoke tests * fixing types * removing Joi extensions * updating tests * documenting changes * fixing NP validation bypass * fix lint problems * Update src/legacy/server/http/integration_tests/xsrf.test.js * Update src/legacy/server/http/integration_tests/xsrf.test.js * revert test changes * simplifying tests Co-authored-by: Elastic Machine --- .../server/http/base_path_proxy_server.ts | 2 + src/core/server/http/http_server.ts | 7 ++ src/core/server/http/http_tools.ts | 6 ++ .../validate_object.test.ts.snap | 13 +++ .../server/http/prototype_pollution/index.ts | 20 +++++ .../validate_object.test.ts | 79 ++++++++++++++++++ .../prototype_pollution/validate_object.ts | 80 +++++++++++++++++++ .../console/server/proxy_route.js | 1 + .../http/integration_tests/xsrf.test.js | 6 +- test/api_integration/apis/general/index.js | 1 + .../apis/general/prototype_pollution.ts | 57 +++++++++++++ .../api_integration/ftr_provider_context.d.ts | 24 ++++++ 12 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap create mode 100644 src/core/server/http/prototype_pollution/index.ts create mode 100644 src/core/server/http/prototype_pollution/validate_object.test.ts create mode 100644 src/core/server/http/prototype_pollution/validate_object.ts create mode 100644 test/api_integration/apis/general/prototype_pollution.ts create mode 100644 test/api_integration/ftr_provider_context.d.ts diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index ff7fee0198f68..cde35f3cbe995 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -143,6 +143,7 @@ export class BasePathProxyServer { return responseToolkit.continue; }, ], + validate: { payload: true }, }, path: `${this.httpConfig.basePath}/{kbnPath*}`, }); @@ -175,6 +176,7 @@ export class BasePathProxyServer { return responseToolkit.continue; }, ], + validate: { payload: true }, }, path: `/__UNSAFE_bypassBasePath/{kbnPath*}`, }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 3354324c12407..da97ab535516c 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -128,6 +128,8 @@ export class HttpServer { for (const route of router.getRoutes()) { this.log.debug(`registering route handler for [${route.path}]`); const { authRequired = true, tags } = route.options; + // Hapi does not allow payload validation to be specified for 'head' or 'get' requests + const validate = ['head', 'get'].includes(route.method) ? undefined : { payload: true }; this.server.route({ handler: route.handler, method: route.method, @@ -135,6 +137,11 @@ export class HttpServer { options: { auth: authRequired ? undefined : false, tags: tags ? Array.from(tags) : undefined, + // TODO: This 'validate' section can be removed once the legacy platform is completely removed. + // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default + // validation applied in ./http_tools#getServerOptions + // (All NP routes are already required to specify their own validation in order to access the payload) + validate, }, }); } diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 88164a76c66f0..22468a5b252f4 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -23,6 +23,7 @@ import Hoek from 'hoek'; import { ServerOptions as TLSOptions } from 'https'; import { ValidationError } from 'joi'; import { HttpConfig } from './http_config'; +import { validateObject } from './prototype_pollution'; /** * Converts Kibana `HttpConfig` into `ServerOptions` that are accepted by the Hapi server. @@ -45,6 +46,11 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = { options: { abortEarly: false, }, + // TODO: This payload validation can be removed once the legacy platform is completely removed. + // This is a default payload validation which applies to all LP routes which do not specify their own + // `validate.payload` handler, in order to reduce the likelyhood of prototype pollution vulnerabilities. + // (All NP routes are already required to specify their own validation in order to access the payload) + payload: value => Promise.resolve(validateObject(value)), }, }, state: { diff --git a/src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap b/src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap new file mode 100644 index 0000000000000..937e040c771ee --- /dev/null +++ b/src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`can't submit {"__proto__":null} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"constructor":{"prototype":null}} 1`] = `"'constructor.prototype' is an invalid key"`; + +exports[`can't submit {"foo":{"__proto__":true}} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"foo":{"bar":{"__proto__":{}}}} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"foo":{"bar":{"constructor":{"prototype":null}}}} 1`] = `"'constructor.prototype' is an invalid key"`; + +exports[`can't submit {"foo":{"constructor":{"prototype":null}}} 1`] = `"'constructor.prototype' is an invalid key"`; diff --git a/src/core/server/http/prototype_pollution/index.ts b/src/core/server/http/prototype_pollution/index.ts new file mode 100644 index 0000000000000..e1a33ffba155e --- /dev/null +++ b/src/core/server/http/prototype_pollution/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { validateObject } from './validate_object'; diff --git a/src/core/server/http/prototype_pollution/validate_object.test.ts b/src/core/server/http/prototype_pollution/validate_object.test.ts new file mode 100644 index 0000000000000..9e23d6cec6444 --- /dev/null +++ b/src/core/server/http/prototype_pollution/validate_object.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { validateObject } from './validate_object'; + +test(`fails on circular references`, () => { + const foo: Record = {}; + foo.myself = foo; + + expect(() => + validateObject({ + payload: foo, + }) + ).toThrowErrorMatchingInlineSnapshot(`"circular reference detected"`); +}); + +[ + { + foo: true, + bar: '__proto__', + baz: 1.1, + qux: undefined, + quux: () => null, + quuz: Object.create(null), + }, + { + foo: { + foo: true, + bar: '__proto__', + baz: 1.1, + qux: undefined, + quux: () => null, + quuz: Object.create(null), + }, + }, + { constructor: { foo: { prototype: null } } }, + { prototype: { foo: { constructor: null } } }, +].forEach(value => { + ['headers', 'payload', 'query', 'params'].forEach(property => { + const obj = { + [property]: value, + }; + test(`can submit ${JSON.stringify(obj)}`, () => { + expect(() => validateObject(obj)).not.toThrowError(); + }); + }); +}); + +// if we use the object literal syntax to create the following values, we end up +// actually reassigning the __proto__ which makes it be a non-enumerable not-own property +// which isn't what we want to test here +[ + JSON.parse(`{ "__proto__": null }`), + JSON.parse(`{ "foo": { "__proto__": true } }`), + JSON.parse(`{ "foo": { "bar": { "__proto__": {} } } }`), + JSON.parse(`{ "constructor": { "prototype" : null } }`), + JSON.parse(`{ "foo": { "constructor": { "prototype" : null } } }`), + JSON.parse(`{ "foo": { "bar": { "constructor": { "prototype" : null } } } }`), +].forEach(value => { + test(`can't submit ${JSON.stringify(value)}`, () => { + expect(() => validateObject(value)).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/src/core/server/http/prototype_pollution/validate_object.ts b/src/core/server/http/prototype_pollution/validate_object.ts new file mode 100644 index 0000000000000..cab6ce295ce92 --- /dev/null +++ b/src/core/server/http/prototype_pollution/validate_object.ts @@ -0,0 +1,80 @@ +/* + * 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. + */ + +interface StackItem { + value: any; + previousKey: string | null; +} + +// we have to do Object.prototype.hasOwnProperty because when you create an object using +// Object.create(null), and I assume other methods, you get an object without a prototype, +// so you can't use current.hasOwnProperty +const hasOwnProperty = (obj: any, property: string) => + Object.prototype.hasOwnProperty.call(obj, property); + +const isObject = (obj: any) => typeof obj === 'object' && obj !== null; + +// we're using a stack instead of recursion so we aren't limited by the call stack +export function validateObject(obj: any) { + if (!isObject(obj)) { + return; + } + + const stack: StackItem[] = [ + { + value: obj, + previousKey: null, + }, + ]; + const seen = new WeakSet([obj]); + + while (stack.length > 0) { + const { value, previousKey } = stack.pop()!; + + if (!isObject(value)) { + continue; + } + + if (hasOwnProperty(value, '__proto__')) { + throw new Error(`'__proto__' is an invalid key`); + } + + if (hasOwnProperty(value, 'prototype') && previousKey === 'constructor') { + throw new Error(`'constructor.prototype' is an invalid key`); + } + + // iterating backwards through an array is reportedly more performant + const entries = Object.entries(value); + for (let i = entries.length - 1; i >= 0; --i) { + const [key, childValue] = entries[i]; + if (isObject(childValue)) { + if (seen.has(childValue)) { + throw new Error('circular reference detected'); + } + + seen.add(childValue); + } + + stack.push({ + value: childValue, + previousKey: key, + }); + } + } +} diff --git a/src/legacy/core_plugins/console/server/proxy_route.js b/src/legacy/core_plugins/console/server/proxy_route.js index 8ce828879a677..856128f3d4c03 100644 --- a/src/legacy/core_plugins/console/server/proxy_route.js +++ b/src/legacy/core_plugins/console/server/proxy_route.js @@ -71,6 +71,7 @@ export const createProxyRoute = ({ parse: false, }, validate: { + payload: true, query: Joi.object() .keys({ method: Joi.string() diff --git a/src/legacy/server/http/integration_tests/xsrf.test.js b/src/legacy/server/http/integration_tests/xsrf.test.js index 562a94e198631..baeb61bff6113 100644 --- a/src/legacy/server/http/integration_tests/xsrf.test.js +++ b/src/legacy/server/http/integration_tests/xsrf.test.js @@ -57,7 +57,8 @@ describe('xsrf request filter', () => { // Disable payload parsing to make HapiJS server accept any content-type header. payload: { parse: false - } + }, + validate: { payload: null } }, handler: async function () { return 'ok'; @@ -71,7 +72,8 @@ describe('xsrf request filter', () => { // Disable payload parsing to make HapiJS server accept any content-type header. payload: { parse: false - } + }, + validate: { payload: null } }, handler: async function () { return 'ok'; diff --git a/test/api_integration/apis/general/index.js b/test/api_integration/apis/general/index.js index 86b7565cba6de..f8daff1a6e8a8 100644 --- a/test/api_integration/apis/general/index.js +++ b/test/api_integration/apis/general/index.js @@ -21,5 +21,6 @@ export default function ({ loadTestFile }) { describe('general', () => { loadTestFile(require.resolve('./cookies')); loadTestFile(require.resolve('./csp')); + loadTestFile(require.resolve('./prototype_pollution')); }); } diff --git a/test/api_integration/apis/general/prototype_pollution.ts b/test/api_integration/apis/general/prototype_pollution.ts new file mode 100644 index 0000000000000..1b732dc81afa9 --- /dev/null +++ b/test/api_integration/apis/general/prototype_pollution.ts @@ -0,0 +1,57 @@ +/* + * 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 { FtrProviderContext } from 'test/api_integration/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('prototype pollution smoke test', () => { + it('prevents payloads with the "constructor.prototype" pollution vector from being accepted', async () => { + await supertest + .post('/api/sample_data/some_data_id') + .send([ + { + constructor: { + prototype: 'foo', + }, + }, + ]) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: "'constructor.prototype' is an invalid key", + validation: { source: 'payload', keys: [] }, + }); + }); + + it('prevents payloads with the "__proto__" pollution vector from being accepted', async () => { + await supertest + .post('/api/sample_data/some_data_id') + .send(JSON.parse(`{"foo": { "__proto__": {} } }`)) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: "'__proto__' is an invalid key", + validation: { source: 'payload', keys: [] }, + }); + }); + }); +} diff --git a/test/api_integration/ftr_provider_context.d.ts b/test/api_integration/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..60f4914a1d27e --- /dev/null +++ b/test/api_integration/ftr_provider_context.d.ts @@ -0,0 +1,24 @@ +/* + * 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; From a32213f849f8b7b4725459e6f475e945328c9356 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 15 Nov 2019 10:55:57 -0500 Subject: [PATCH 18/44] Retry git clone up to 8 times before failing a build (#50734) --- vars/kibanaPipeline.groovy | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index e8d7cc03edad0..f824acbc63ccd 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -127,7 +127,17 @@ def jobRunner(label, useRamDisk, closure) { } } - def scmVars = checkout scm + def scmVars + + // Try to clone from Github up to 8 times, waiting 15 secs between attempts + retry(8) { + try { + scmVars = checkout scm + } catch (ex) { + sleep 15 + throw ex + } + } withEnv([ "CI=true", From 504604a4f2dc3cd0190f3179e478d5943bc21e73 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 15 Nov 2019 10:59:05 -0500 Subject: [PATCH 19/44] Add labels to shell scripts in Jenkins (#49657) --- .ci/Jenkinsfile_flaky | 10 ++++----- Jenkinsfile | 12 +++++----- vars/kibanaPipeline.groovy | 45 +++++++++++++++++++++++--------------- vars/runbld.groovy | 12 +++++++--- 4 files changed, 47 insertions(+), 32 deletions(-) diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index e1cbac0528b1f..8ad02b7162b6a 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -34,7 +34,7 @@ stage("Kibana Pipeline") { if (!IS_XPACK) { kibanaPipeline.buildOss() if (CI_GROUP == '1') { - runbld "./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh" + runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") } } else { kibanaPipeline.buildXpack() @@ -62,18 +62,18 @@ stage("Kibana Pipeline") { def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld './test/scripts/jenkins_firefox_smoke.sh' }) + return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }) } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld './test/scripts/jenkins_visual_regression.sh' }) + return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }) } else { return kibanaPipeline.getOssCiGroupWorker(ciGroup) } } if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld './test/scripts/jenkins_xpack_firefox_smoke.sh' }) + return kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') }) } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld './test/scripts/jenkins_xpack_visual_regression.sh' }) + return kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }) } else { return kibanaPipeline.getXpackCiGroupWorker(ciGroup) } diff --git a/Jenkinsfile b/Jenkinsfile index 8d8579736f639..c002832d4d51a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -24,9 +24,9 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), - 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld './test/scripts/jenkins_firefox_smoke.sh' }), - 'oss-accessibility': kibanaPipeline.getPostBuildWorker('accessibility', { runbld './test/scripts/jenkins_accessibility.sh' }), - 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld './test/scripts/jenkins_visual_regression.sh' }), + 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }), + 'oss-accessibility': kibanaPipeline.getPostBuildWorker('accessibility', { runbld('./test/scripts/jenkins_accessibility.sh', 'Execute kibana-accessibility') }), + 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }), ]), 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), @@ -39,9 +39,9 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8), 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9), 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10), - 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld './test/scripts/jenkins_xpack_firefox_smoke.sh' }), - 'xpack-accessibility': kibanaPipeline.getPostBuildWorker('xpack-accessibility', { runbld './test/scripts/jenkins_xpack_accessibility.sh' }), - 'xpack-visualRegression': kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld './test/scripts/jenkins_xpack_visual_regression.sh' }), + 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') }), + 'xpack-accessibility': kibanaPipeline.getPostBuildWorker('xpack-accessibility', { runbld('./test/scripts/jenkins_xpack_accessibility.sh', 'Execute xpack-accessibility') }), + 'xpack-visualRegression': kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }), ]), ]) } diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index f824acbc63ccd..90df352e18a00 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -68,7 +68,7 @@ def getOssCiGroupWorker(ciGroup) { "CI_GROUP=${ciGroup}", "JOB=kibana-ciGroup${ciGroup}", ]) { - runbld "./test/scripts/jenkins_ci_group.sh" + runbld("./test/scripts/jenkins_ci_group.sh", "Execute kibana-ciGroup${ciGroup}") } }) } @@ -79,7 +79,7 @@ def getXpackCiGroupWorker(ciGroup) { "CI_GROUP=${ciGroup}", "JOB=xpack-kibana-ciGroup${ciGroup}", ]) { - runbld "./test/scripts/jenkins_xpack_ci_group.sh" + runbld("./test/scripts/jenkins_xpack_ci_group.sh", "Execute xpack-kibana-ciGroup${ciGroup}") } }) } @@ -93,7 +93,7 @@ def legacyJobRunner(name) { ]) { jobRunner('linux && immutable', false) { try { - runbld('.ci/run.sh', true) + runbld('.ci/run.sh', "Execute ${name}", true) } finally { catchError { uploadAllGcsArtifacts(name) @@ -118,12 +118,15 @@ def jobRunner(label, useRamDisk, closure) { // Move to a temporary workspace, so that we can symlink the real workspace into /dev/shm def originalWorkspace = env.WORKSPACE ws('/tmp/workspace') { - sh """ - mkdir -p /dev/shm/workspace - mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist - rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it - ln -s /dev/shm/workspace '${originalWorkspace}' - """ + sh( + script: """ + mkdir -p /dev/shm/workspace + mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist + rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it + ln -s /dev/shm/workspace '${originalWorkspace}' + """, + label: "Move workspace to RAM - /dev/shm/workspace" + ) } } @@ -235,27 +238,33 @@ def sendKibanaMail() { } } -def bash(script) { - sh "#!/bin/bash\n${script}" +def bash(script, label) { + sh( + script: "#!/bin/bash\n${script}", + label: label + ) } def doSetup() { - runbld "./test/scripts/jenkins_setup.sh" + runbld("./test/scripts/jenkins_setup.sh", "Setup Build Environment and Dependencies") } def buildOss() { - runbld "./test/scripts/jenkins_build_kibana.sh" + runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") } def buildXpack() { - runbld "./test/scripts/jenkins_xpack_build_kibana.sh" + runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") } def runErrorReporter() { - bash """ - source src/dev/ci_setup/setup_env.sh - node scripts/report_failed_tests - """ + bash( + """ + source src/dev/ci_setup/setup_env.sh + node scripts/report_failed_tests + """, + "Report failed tests, if necessary" + ) } return this diff --git a/vars/runbld.groovy b/vars/runbld.groovy index 501e2421ca65b..e52bc244c65cb 100644 --- a/vars/runbld.groovy +++ b/vars/runbld.groovy @@ -1,11 +1,17 @@ -def call(script, enableJunitProcessing = false) { +def call(script, label, enableJunitProcessing = false) { def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" - sh "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}" + sh( + script: "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}", + label: label ?: script + ) } def junit() { - sh "/usr/local/bin/runbld -d '${pwd()}' ${env.WORKSPACE}/kibana/test/scripts/jenkins_runbld_junit.sh" + sh( + script: "/usr/local/bin/runbld -d '${pwd()}' ${env.WORKSPACE}/kibana/test/scripts/jenkins_runbld_junit.sh", + label: "Process JUnit reports with runbld" + ) } return this From 69431b55d59f68e89a040eaf5a8d6aa491211c48 Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Fri, 15 Nov 2019 12:18:49 -0500 Subject: [PATCH 20/44] [Canvas] Use compressed forms in sidebar (#49419) * compressed sidebar design * add back metric help, style no datasource msg * re-style remove button * re-style element status section * remove unused component import * update storyshots * clean up unused i18n values * address feedback * address i81n feedback * update storyshot * convert string to i18n * style grouped sidebar messages * update storyshots --- .../extended_template.examples.storyshot | 10 +- .../axis_config/extended_template.tsx | 8 +- .../uis/arguments/datacolumn/index.js | 2 +- .../uis/arguments/filter_group.js | 6 +- .../uis/arguments/image_upload/index.js | 1 + .../canvas_plugin_src/uis/arguments/string.js | 2 +- .../canvas_plugin_src/uis/arguments/toggle.js | 10 +- .../uis/datasources/demodata.js | 20 +--- .../uis/datasources/essql.js | 17 ++- .../uis/datasources/index.js | 4 +- .../uis/datasources/timelion.js | 68 ++++++------ .../canvas_plugin_src/uis/views/metric.js | 24 ++--- .../canvas/canvas_plugin_src/uis/views/pie.js | 48 ++++----- .../legacy/plugins/canvas/i18n/components.ts | 12 +-- .../legacy/plugins/canvas/i18n/constants.ts | 3 + .../plugins/canvas/i18n/expression_types.ts | 19 ++-- x-pack/legacy/plugins/canvas/i18n/ui.ts | 83 ++++++++------ .../arg_add_popover/arg_add_popover.scss | 4 + .../arg_add_popover/arg_add_popover.tsx | 1 + .../public/components/arg_form/arg_form.scss | 56 ++++------ .../public/components/arg_form/arg_label.js | 14 ++- .../components/arg_form/arg_simple_form.tsx | 19 ++-- .../components/datasource/datasource.scss | 25 +++++ .../datasource/datasource_component.js | 69 +++++++----- .../datasource_preview/datasource_preview.js | 8 +- .../datasource/datasource_selector.js | 13 ++- .../components/datasource/no_datasource.js | 11 +- .../components/datatable/datatable.scss | 9 +- .../element_config/element_config.js | 51 ++++++--- .../es_field_select/es_field_select.js | 1 + .../es_fields_select/es_fields_select.js | 1 + .../es_index_select/es_index_select.js | 1 + .../components/page_config/page_config.js | 26 +++-- .../shape_picker_popover.examples.storyshot | 102 ++++++++++-------- .../shape_picker_popover.tsx | 10 +- .../group_settings.examples.storyshot | 18 ++-- .../multi_element_settings.examples.storyshot | 18 ++-- .../element_settings/element_settings.tsx | 4 +- .../components/sidebar/global_config.tsx | 4 +- .../components/sidebar/group_settings.tsx | 10 +- .../sidebar/multi_element_settings.tsx | 10 +- .../public/components/sidebar/sidebar.scss | 10 +- .../components/sidebar/sidebar_section.js | 4 +- .../sidebar/sidebar_section_title.js | 2 +- .../sidebar_header/sidebar_header.scss | 6 +- .../text_style_picker/text_style_picker.js | 27 ++--- .../workpad_config/workpad_config.js | 71 ++++++------ .../extended_template.examples.storyshot | 6 +- .../simple_template.examples.storyshot | 90 ++++------------ .../series_style/extended_template.tsx | 2 +- .../series_style/simple_template.tsx | 14 +-- .../expression_types/datasources/esdocs.js | 55 +++++++--- .../plugins/canvas/public/style/main.scss | 2 +- 53 files changed, 628 insertions(+), 483 deletions(-) diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot index 5efbbe0106505..ef301c08cdfe8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot @@ -11,7 +11,7 @@ exports[`Storyshots arguments/AxisConfig extended 1`] = ` } >
- The axis is disabled +

+ Switch on to view axis settings +

diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx index 0ec722e370b40..832f953974af4 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx @@ -71,7 +71,11 @@ export class ExtendedTemplate extends PureComponent { const isDisabled = typeof this.props.argValue === 'boolean' && this.props.argValue === false; if (isDisabled) { - return The axis is disabled; + return ( + +

{strings.getDisabledText()}

+
+ ); } const positions = { @@ -85,7 +89,7 @@ export class ExtendedTemplate extends PureComponent { return ( - + + { onChange={ev => setInputValue(ev.target.value)} /> - + - Set + {strings.getButtonSet()} setAddMode(!addMode)} flush="left"> - Cancel + {strings.getButtonCancel()} ); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js index e8c433fb8752d..a3c327da2e4dc 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js @@ -130,6 +130,7 @@ class ImageUpload extends React.Component { idSelected={urlType} onChange={this.changeUrlType} isFullWidth + className="canvasSidebar__buttonGroup" /> ); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js index d8a7188dfab28..dc31497a7da78 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js @@ -26,7 +26,7 @@ const StringArgInput = ({ updateValue, value, confirm, commit, argId }) => ( /> {confirm && ( - + commit(value)}> {confirm} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js index 462537e82b164..de19d3e29221b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js @@ -19,8 +19,14 @@ const ToggleArgInput = ({ onValueChange, argValue, argId, renderError }) => { return null; } return ( - - + + ); }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js index ec492f52747c1..193d99e1c9533 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js @@ -5,28 +5,15 @@ */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; -import { ComponentStrings, CANVAS, DataSourceStrings } from '../../../i18n'; +import { DataSourceStrings } from '../../../i18n'; const { DemoData: strings } = DataSourceStrings; const DemodataDatasource = () => ( - -

{strings.getHeading()}

-

- {ComponentStrings.DatasourceDatasourceComponent.getChangeButtonLabel()} - ), - }} - /> -

+ +

{strings.getDescription()}

); @@ -34,7 +21,6 @@ export const demodata = () => ({ name: 'demodata', displayName: strings.getDisplayName(), help: strings.getHelp(), - // Replace this with a better icon when we have time. image: 'logoElasticStack', template: templateFromReactComponent(DemodataDatasource), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js index 43f2fa63aff70..707f2305e1368 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js @@ -6,10 +6,10 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { EuiFormRow, EuiTextArea } from '@elastic/eui'; +import { EuiFormRow, EuiTextArea, EuiLink, EuiText } from '@elastic/eui'; import { getSimpleArg, setSimpleArg } from '../../../public/lib/arg_helpers'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; -import { DataSourceStrings } from '../../../i18n'; +import { DataSourceStrings, SQL_URL } from '../../../i18n'; const { Essql: strings } = DataSourceStrings; @@ -59,13 +59,24 @@ class EssqlDatasource extends PureComponent { const { isInvalid } = this.props; return ( - + + + {strings.getLabelAppend()} + +
+ } + >
); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js index 107d4d241d2e7..13aa2a06306a0 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { timelion } from './timelion'; import { demodata } from './demodata'; import { essql } from './essql'; +import { timelion } from './timelion'; -export const datasourceSpecs = [timelion, demodata, essql]; +export const datasourceSpecs = [demodata, essql, timelion]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js index 06efb6a791a2d..b30e43c1c3c57 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js @@ -12,13 +12,13 @@ import { EuiCallOut, EuiSpacer, EuiCode, - EuiText, EuiTextArea, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { getSimpleArg, setSimpleArg } from '../../../public/lib/arg_helpers'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; import { DataSourceStrings, TIMELION, CANVAS } from '../../../i18n'; +import { TooltipIcon } from '../../../public/components/tooltip_icon'; const { Timelion: strings } = DataSourceStrings; @@ -57,43 +57,12 @@ const TimelionDatasource = ({ args, updateArgs, defaultIndex }) => { return (
- -

{TIMELION}

-

{strings.getAbout()}

-
- - - - - setArg(argName, e.target.value)} - /> - - { - // TODO: Time timelion interval picker should be a drop down - } - - setArg('interval', e.target.value)} - /> - - - - - +
  • {
  • {
+ + + + } + > + setArg(argName, e.target.value)} + rows={15} + /> + + { + // TODO: Time timelion interval picker should be a drop down + } + + setArg('interval', e.target.value)} + /> +
); }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js index 213a2e0dd3b81..33cdb5541e172 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js @@ -16,6 +16,13 @@ export const metric = () => ({ modelArgs: [['_', { label: strings.getNumberDisplayName() }]], requiresContext: false, args: [ + { + name: 'metricFormat', + displayName: strings.getMetricFormatDisplayName(), + help: strings.getMetricFormatHelp(), + argType: 'numberFormat', + default: `"${AdvancedSettings.get('format:number:defaultPattern')}"`, + }, { name: '_', displayName: strings.getLabelDisplayName(), @@ -23,13 +30,6 @@ export const metric = () => ({ argType: 'string', default: '""', }, - { - name: 'labelFont', - displayName: strings.getLabelFontDisplayName(), - help: strings.getLabelFontHelp(), - argType: 'font', - default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, - }, { name: 'metricFont', displayName: strings.getMetricFontDisplayName(), @@ -38,11 +38,11 @@ export const metric = () => ({ default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, }, { - name: 'metricFormat', - displayName: strings.getMetricFormatDisplayName(), - help: strings.getMetricFormatHelp(), - argType: 'numberFormat', - default: `"${AdvancedSettings.get('format:number:defaultPattern')}"`, + name: 'labelFont', + displayName: strings.getLabelFontDisplayName(), + help: strings.getLabelFontHelp(), + argType: 'font', + default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, }, ], }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js index 4bb68973e80ea..783140b0c8b9e 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js @@ -23,6 +23,16 @@ export const pie = () => ({ name: 'palette', argType: 'palette', }, + { + name: 'legend', + displayName: strings.getLegendDisplayName(), + help: strings.getLegendHelp(), + argType: 'select', + default: 'ne', + options: { + choices: legendOptions, + }, + }, { name: 'hole', displayName: strings.getHoleDisplayName(), @@ -34,13 +44,6 @@ export const pie = () => ({ max: 100, }, }, - { - name: 'labels', - displayName: strings.getLabelsDisplayName(), - help: strings.getLabelsHelp(), - argType: 'toggle', - default: true, - }, { name: 'labelRadius', displayName: strings.getLabelRadiusDisplayName(), @@ -52,16 +55,6 @@ export const pie = () => ({ max: 100, }, }, - { - name: 'legend', - displayName: strings.getLegendDisplayName(), - help: strings.getLegendHelp(), - argType: 'select', - default: 'ne', - options: { - choices: legendOptions, - }, - }, { name: 'radius', displayName: strings.getRadiusDisplayName(), @@ -69,6 +62,20 @@ export const pie = () => ({ argType: 'percentage', default: 1, }, + { + name: 'tilt', + displayName: strings.getTiltDisplayName(), + help: strings.getTiltHelp(), + argType: 'percentage', + default: 1, + }, + { + name: 'labels', + displayName: strings.getLabelsDisplayName(), + help: strings.getLabelsHelp(), + argType: 'toggle', + default: true, + }, { name: 'seriesStyle', argType: 'seriesStyle', @@ -78,13 +85,6 @@ export const pie = () => ({ name: 'font', argType: 'font', }, - { - name: 'tilt', - displayName: strings.getTiltDisplayName(), - help: strings.getTiltHelp(), - argType: 'percentage', - default: 1, - }, ], resolve({ context }) { if (getState(context) !== 'ready') { diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts index 1e6da888abf58..5b9f6f00940f4 100644 --- a/x-pack/legacy/plugins/canvas/i18n/components.ts +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -228,11 +228,11 @@ export const ComponentStrings = { DatasourceDatasourceComponent: { getChangeButtonLabel: () => i18n.translate('xpack.canvas.datasourceDatasourceComponent.changeButtonLabel', { - defaultMessage: 'Change your data source', + defaultMessage: 'Change element data source', }), getPreviewButtonLabel: () => i18n.translate('xpack.canvas.datasourceDatasourceComponent.previewButtonLabel', { - defaultMessage: 'Preview', + defaultMessage: 'Preview data', }), getSaveButtonLabel: () => i18n.translate('xpack.canvas.datasourceDatasourceComponent.saveButtonLabel', { @@ -294,7 +294,7 @@ export const ComponentStrings = { }), getTitle: () => i18n.translate('xpack.canvas.elementConfig.title', { - defaultMessage: 'Elements', + defaultMessage: 'Element status', description: '"Elements" refers to the individual text, images, or visualizations that you can add to a Canvas workpad', }), @@ -581,7 +581,7 @@ export const ComponentStrings = { }), getBackgroundColorLabel: () => i18n.translate('xpack.canvas.pageConfig.backgroundColorLabel', { - defaultMessage: 'Background Color', + defaultMessage: 'Background', }), getNoTransitionDropDownOptionLabel: () => i18n.translate('xpack.canvas.pageConfig.transitions.noneDropDownOptionLabel', { @@ -592,7 +592,7 @@ export const ComponentStrings = { }), getTitle: () => i18n.translate('xpack.canvas.pageConfig.title', { - defaultMessage: 'Page', + defaultMessage: 'Page styles', }), getTransitionLabel: () => i18n.translate('xpack.canvas.pageConfig.transitionLabel', { @@ -1002,7 +1002,7 @@ export const ComponentStrings = { }), getTitle: () => i18n.translate('xpack.canvas.workpadConfig.title', { - defaultMessage: 'Workpad', + defaultMessage: 'Workpad settings', }), getUSLetterButtonLabel: () => i18n.translate('xpack.canvas.workpadConfig.USLetterButtonLabel', { diff --git a/x-pack/legacy/plugins/canvas/i18n/constants.ts b/x-pack/legacy/plugins/canvas/i18n/constants.ts index 3659c369ba0b6..8aee6ca148681 100644 --- a/x-pack/legacy/plugins/canvas/i18n/constants.ts +++ b/x-pack/legacy/plugins/canvas/i18n/constants.ts @@ -15,6 +15,7 @@ export const CSV = 'CSV'; export const DATEMATH = '`datemath`'; export const DATATABLE = '`datatable`'; export const ELASTICSEARCH = 'Elasticsearch'; +export const ELASTICSEARCH_SHORT = 'ES'; export const FONT_FAMILY = '`font-family`'; export const FONT_WEIGHT = '`font-weight`'; export const HEX = 'HEX'; @@ -32,6 +33,8 @@ export const PDF = 'PDF'; export const POST = 'POST'; export const RGB = 'RGB'; export const SQL = 'SQL'; +export const SQL_URL = + 'https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-spec.html'; export const SVG = 'SVG'; export const TIMELION = 'Timelion'; export const TINYMATH = '`TinyMath`'; diff --git a/x-pack/legacy/plugins/canvas/i18n/expression_types.ts b/x-pack/legacy/plugins/canvas/i18n/expression_types.ts index 6bc40a2758ab3..bdd190f26c97a 100644 --- a/x-pack/legacy/plugins/canvas/i18n/expression_types.ts +++ b/x-pack/legacy/plugins/canvas/i18n/expression_types.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { LUCENE } from './constants'; +import { LUCENE, ELASTICSEARCH } from './constants'; export const ArgTypesStrings = { Color: { @@ -101,13 +101,17 @@ export const ArgTypesStrings = { i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.colorLabel', { defaultMessage: 'Color', }), + getColorValueDefault: () => + i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.colorValueDefault', { + defaultMessage: 'Auto', + }), getStyleLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.styleLabel', { defaultMessage: 'Style', }), getRemoveAriaLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.removeAriaLabel', { - defaultMessage: 'Remove Series Color', + defaultMessage: 'Remove series color', }), getNoSeriesTooltip: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.noSeriesTooltip', { @@ -115,11 +119,11 @@ export const ArgTypesStrings = { }), getSeriesIdentifierLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.seriesIdentifierLabel', { - defaultMessage: 'Series Identifier', + defaultMessage: 'Series id', }), getSelectSeriesOption: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.selectSeriesDropDown', { - defaultMessage: 'Select Series', + defaultMessage: 'Select series', }), getLineLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.lineLabel', { @@ -152,15 +156,18 @@ export const ExpressionDataSourceStrings = { }), getWarningTitle: () => i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.warningTitle', { - defaultMessage: 'Be careful', + defaultMessage: 'Query with caution', }), getWarning: () => i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.warningDescription', { defaultMessage: ` - The Elasticsearch Docs datasource is used to pull documents directly from Elasticsearch + This datasource pulls directly from {elasticsearch} without the use of aggregations. It is best used with low volume datasets and in situations where you need to view raw documents or plot exact, non-aggregated values on a chart.`, + values: { + elasticsearch: ELASTICSEARCH, + }, }), getIndexTitle: () => i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.indexTitle', { diff --git a/x-pack/legacy/plugins/canvas/i18n/ui.ts b/x-pack/legacy/plugins/canvas/i18n/ui.ts index b65a666aa8809..323a6c97fd967 100644 --- a/x-pack/legacy/plugins/canvas/i18n/ui.ts +++ b/x-pack/legacy/plugins/canvas/i18n/ui.ts @@ -12,9 +12,9 @@ import { CANVAS, CSS, ELASTICSEARCH, + ELASTICSEARCH_SHORT, HEX, HTML, - KIBANA, LUCENE, MARKDOWN, MOMENTJS, @@ -35,6 +35,10 @@ export const ArgumentStrings = { i18n.translate('xpack.canvas.uis.arguments.axisConfigLabel', { defaultMessage: 'Visualization axis configuration', }), + getDisabledText: () => + i18n.translate('xpack.canvas.uis.arguments.axisConfigDisabledText', { + defaultMessage: 'Switch on to view axis settings', + }), getPositionBottom: () => i18n.translate('xpack.canvas.uis.arguments.axisConfig.position.options.bottomDropDown', { defaultMessage: 'bottom', @@ -124,6 +128,14 @@ export const ArgumentStrings = { i18n.translate('xpack.canvas.uis.arguments.filterGroup.createNewGroupLinkText', { defaultMessage: 'Create new group', }), + getButtonSet: () => + i18n.translate('xpack.canvas.uis.arguments.filterGroup.setValue', { + defaultMessage: 'Set', + }), + getButtonCancel: () => + i18n.translate('xpack.canvas.uis.arguments.filterGroup.cancelValue', { + defaultMessage: 'Cancel', + }), getDisplayName: () => i18n.translate('xpack.canvas.uis.arguments.filterGroupTitle', { defaultMessage: 'Filter Group', @@ -260,7 +272,7 @@ export const ArgumentStrings = { }), getHelp: () => i18n.translate('xpack.canvas.uis.arguments.shapeLabel', { - defaultMessage: 'Shape picker', + defaultMessage: 'Change the shape of the current element', }), }, String: { @@ -303,12 +315,20 @@ export const DataSourceStrings = { }), getHeading: () => i18n.translate('xpack.canvas.uis.dataSources.demoData.headingTitle', { - defaultMessage: 'You are using demo data', + defaultMessage: 'This element is using demo data', }), getHelp: () => i18n.translate('xpack.canvas.uis.dataSources.demoDataLabel', { defaultMessage: 'Mock data set with usernames, prices, projects, countries, and phases', }), + getDescription: () => + i18n.translate('xpack.canvas.uis.dataSources.demoDataDescription', { + defaultMessage: + 'By default, every {canvas} element is connected to the demo data source. Change the data source, above, to connect your own data.', + values: { + canvas: CANVAS, + }, + }), }, Essql: { getDisplayName: () => @@ -329,9 +349,13 @@ export const DataSourceStrings = { }), getLabel: () => i18n.translate('xpack.canvas.uis.dataSources.essql.queryTitle', { - defaultMessage: '{elasticsearch} {sql} query', + defaultMessage: 'Query', + }), + getLabelAppend: () => + i18n.translate('xpack.canvas.uis.dataSources.essql.queryTitleAppend', { + defaultMessage: 'Learn {elasticsearchShort} {sql} syntax', values: { - elasticsearch: ELASTICSEARCH, + elasticsearchShort: ELASTICSEARCH_SHORT, sql: SQL, }, }), @@ -340,10 +364,9 @@ export const DataSourceStrings = { getAbout: () => i18n.translate('xpack.canvas.uis.dataSources.timelion.aboutDetail', { defaultMessage: - '{canvas} integrates with {kibanaTimelion} application to allow you to use {timelion} queries to pull back timeseries data in a tabular format that can be used with {canvas} elements.', + 'Use {timelion} queries to pull back timeseries data that can be used with {canvas} elements.', values: { timelion: TIMELION, - kibanaTimelion: `${KIBANA}'s ${TIMELION}`, canvas: CANVAS, }, }), @@ -357,9 +380,8 @@ export const DataSourceStrings = { getIntervalHelp: () => i18n.translate('xpack.canvas.uis.dataSources.timelion.intervalLabel', { defaultMessage: - 'Accepts {elasticsearch} date math: {weeksExample}, {daysExample}, {secondsExample}, or {auto}', + 'Use date math like {weeksExample}, {daysExample}, {secondsExample}, or {auto}', values: { - elasticsearch: ELASTICSEARCH, secondsExample: '10s', daysExample: '5d', weeksExample: '1w', @@ -383,7 +405,11 @@ export const DataSourceStrings = { }), getTipsHeading: () => i18n.translate('xpack.canvas.uis.dataSources.timelion.tipsTitle', { - defaultMessage: 'Some tips', + defaultMessage: 'Tips for using {timelion} in {canvas}', + values: { + timelion: TIMELION, + canvas: CANVAS, + }, }), }, }; @@ -530,7 +556,7 @@ export const ViewStrings = { }), getValueDisplayName: () => i18n.translate('xpack.canvas.uis.views.dropdownControl.args.valueColumnTitle', { - defaultMessage: 'Values column', + defaultMessage: 'Value column', }), getValueHelp: () => i18n.translate('xpack.canvas.uis.views.dropdownControl.args.valueColumnLabel', { @@ -610,7 +636,7 @@ export const ViewStrings = { }), getNumberDisplayName: () => i18n.translate('xpack.canvas.uis.views.numberArgTitle', { - defaultMessage: 'Number', + defaultMessage: 'Value', }), getLabelDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelArgTitle', { @@ -618,7 +644,7 @@ export const ViewStrings = { }), getLabelFontDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelFontTitle', { - defaultMessage: 'Label text settings', + defaultMessage: 'Label text', }), getLabelFontHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelFontLabel', { @@ -626,11 +652,11 @@ export const ViewStrings = { }), getLabelHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelArgLabel', { - defaultMessage: 'Describes the metric', + defaultMessage: 'Enter a text label for the metric value', }), getMetricFontDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFontTitle', { - defaultMessage: 'Metric text settings', + defaultMessage: 'Metric text', }), getMetricFontHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFontLabel', { @@ -638,11 +664,11 @@ export const ViewStrings = { }), getMetricFormatDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFormatTitle', { - defaultMessage: 'Metric Format', + defaultMessage: 'Format', }), getMetricFormatHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFormatLabel', { - defaultMessage: 'Fonts, alignment and color', + defaultMessage: 'Select a format for the metric value', }), }, Pie: { @@ -676,7 +702,7 @@ export const ViewStrings = { }), getLegendDisplayName: () => i18n.translate('xpack.canvas.uis.views.pie.args.legendTitle', { - defaultMessage: 'Legend position', + defaultMessage: 'Legend', }), getLegendHelp: () => i18n.translate('xpack.canvas.uis.views.pie.args.legendLabel', { @@ -714,7 +740,7 @@ export const ViewStrings = { }), getLegendDisplayName: () => i18n.translate('xpack.canvas.uis.views.plot.args.legendTitle', { - defaultMessage: 'Legend position', + defaultMessage: 'Legend', }), getLegendHelp: () => i18n.translate('xpack.canvas.uis.views.plot.args.legendLabel', { @@ -744,7 +770,7 @@ export const ViewStrings = { }), getBarColorHelp: () => i18n.translate('xpack.canvas.uis.views.progress.args.barColorLabel', { - defaultMessage: 'Accepts HEX, RGB or HTML Color names', + defaultMessage: 'Accepts HEX, RGB or HTML color names', }), getBarWeightDisplayName: () => i18n.translate('xpack.canvas.uis.views.progress.args.barWeightTitle', { @@ -930,7 +956,7 @@ export const ViewStrings = { }), getBorderHelp: () => i18n.translate('xpack.canvas.uis.views.shape.args.borderLabel', { - defaultMessage: 'Accepts HEX, RGB or HTML Color names', + defaultMessage: 'Accepts HEX, RGB or HTML color names', }), getBorderWidthDisplayName: () => i18n.translate('xpack.canvas.uis.views.shape.args.borderWidthTitle', { @@ -950,22 +976,19 @@ export const ViewStrings = { }), getFillHelp: () => i18n.translate('xpack.canvas.uis.views.shape.args.fillLabel', { - defaultMessage: 'Accepts HEX, RGB or HTML Color names', + defaultMessage: 'Accepts HEX, RGB or HTML color names', }), getMaintainAspectDisplayName: () => i18n.translate('xpack.canvas.uis.views.shape.args.maintainAspectTitle', { - defaultMessage: 'Maintain aspect ratio', + defaultMessage: 'Fixed ratio', }), getMaintainAspectHelp: () => i18n.translate('xpack.canvas.uis.views.shape.args.maintainAspectLabel', { - defaultMessage: `Select '{true}' to maintain aspect ratio`, - values: { - true: BOOLEAN_TRUE, - }, + defaultMessage: `Enable to maintain aspect ratio`, }), getShapeDisplayName: () => i18n.translate('xpack.canvas.uis.views.shape.args.shapeTitle', { - defaultMessage: 'Select a shape', + defaultMessage: 'Select shape', }), }, Table: { @@ -988,7 +1011,7 @@ export const ViewStrings = { }), getPerPageDisplayName: () => i18n.translate('xpack.canvas.uis.views.table.args.perPageTitle', { - defaultMessage: 'Rows per page', + defaultMessage: 'Rows', }), getPerPageHelp: () => i18n.translate('xpack.canvas.uis.views.table.args.perPageLabel', { @@ -1022,7 +1045,7 @@ export const ViewStrings = { }), getFilterGroupDisplayName: () => i18n.translate('xpack.canvas.uis.views.timefilter.args.filterGroupTitle', { - defaultMessage: 'Filter group name', + defaultMessage: 'Filter group', }), getFilterGroupHelp: () => i18n.translate('xpack.canvas.uis.views.timefilter.args.filterGroupLabel', { diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss index f2cdb1444ef23..15676d2b02490 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss +++ b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss @@ -1,3 +1,7 @@ +.canvasArg__addArg { + margin-right: -$euiSizeS; +} + .canvasArg__addPopover { width: 250px; } diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx index e771c978d491b..9cc6f870b9bde 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx @@ -33,6 +33,7 @@ export const ArgAddPopover = ({ options }: Props) => { iconType="plusInCircle" aria-label={strings.getAddAriaLabel()} onClick={handleClick} + className="canvasArg__addArg" /> ); diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss index bef58d6bb6f5f..3fc220d41d551 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss +++ b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss @@ -8,6 +8,14 @@ .canvasSidebar__panel { .canvasArg--expandable:last-child { + .canvasArg__accordion { + margin-bottom: (-$euiSizeS); + } + + .canvasArg__accordion:after { + content: none; + } + .canvasArg__accordion.euiAccordion-isOpen:after { display: none; } @@ -15,12 +23,12 @@ } .canvasArg { - margin-top: $euiSize; - + margin-top: $euiSizeS; +} - .canvasArg--remove { - visibility: hidden; - } +.canvasArg__remove { + min-width: $euiSize; + padding: $euiSizeXS 0; } .canvasArg__content { @@ -29,47 +37,18 @@ .canvasArg__form { position: relative; - -} - -.canvasArg__form, -.canvasArg__accordion { - &:hover { - .canvasArg__remove { - opacity: 1; - visibility: visible; - } - } } .canvasArg__tooltip { margin-left: -$euiSizeXL; } -.canvasArg__remove { - position: absolute; - right: -$euiSizeL; - top: $euiSizeS - 2px; - border-radius: $euiBorderRadius; - border: $euiBorderThin; - background: $euiColorEmptyShade; - opacity: 0; - visibility: hidden; - transition: opacity $euiAnimSpeedNormal $euiAnimSlightResistance; - transition-delay: $euiAnimSpeedSlow; -} - .canvasArg__accordion { - padding: $euiSizeS $euiSize; - margin: 0 (-$euiSize); + padding: $euiSizeS $euiSizeM; + margin: 0 (-$euiSizeM); background: $euiColorLightestShade; position: relative; - // different spacing means leff shift - .canvasArg__remove { - right: -$euiSizeM; - } - // don't let remove button position here if this is nested in an accordion .canvasArg__form { position: static; @@ -97,3 +76,8 @@ bottom: 0; } } + +// this is a workaround since an EuiFormRow label cannot be passed in toggle.js +.canvasArg__switch { + padding-top: calc(#{$euiSizeS} * .75); +} diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js index 143ce670d25f6..4324eed0892a5 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js +++ b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js @@ -6,7 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiFormRow, EuiAccordion, EuiText, EuiToolTip } from '@elastic/eui'; +import { EuiFormRow, EuiAccordion, EuiText, EuiToolTip, EuiIcon } from '@elastic/eui'; // This is what is being generated by render() from the Arg class. It is called in FunctionForm export const ArgLabel = props => { @@ -32,7 +32,17 @@ export const ArgLabel = props => { ) : ( simpleArg && ( - + + + {label} + + + } + id={argId} + > {simpleArg} ) diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx index 5b45772c14373..846f912db6f84 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx @@ -6,7 +6,7 @@ import React, { ReactNode, MouseEventHandler } from 'react'; import PropTypes from 'prop-types'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { TooltipIcon, IconType } from '../tooltip_icon'; import { ComponentStrings } from '../../../i18n'; @@ -41,13 +41,16 @@ export const ArgSimpleForm: React.FunctionComponent = ({ )} {!required && ( - + + + )} ); diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss index ee6c082db1217..2407dcbbce593 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss @@ -6,6 +6,31 @@ padding: 0 $euiSizeS; } +.canvasDataSource__section { + padding: $euiSizeM; +} + +.canvasDataSource__triggerButton { + @include euiTitle('xs'); + line-height: $euiSizeXXL; +} + +.canvasDataSource__triggerButtonIcon { + margin-right: $euiSizeS; +} + +.canvasDataSource__list { + padding: $euiSizeM; +} + +.canvasDataSource__card .euiCard__content { + padding-top: 0 !important; // sass-lint:disable-line no-important +} + .canvasDataSource__card + .canvasDataSource__card { margin-top: $euiSizeS; } + +.canvasDataSource__card--isCurrent { + border-color: $euiColorSecondary; +} diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js index 5f235d4479171..8b0061e047f33 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js @@ -7,20 +7,23 @@ import React, { Fragment, PureComponent } from 'react'; import PropTypes from 'prop-types'; import { - EuiPanel, EuiFlexGroup, EuiFlexItem, EuiButton, - EuiButtonEmpty, EuiSpacer, + EuiIcon, + EuiCallOut, + EuiButtonEmpty, + EuiHorizontalRule, } from '@elastic/eui'; import { isEqual } from 'lodash'; -import { ComponentStrings } from '../../../i18n'; +import { ComponentStrings, DataSourceStrings } from '../../../i18n'; import { getDefaultIndex } from '../../lib/es_service'; import { DatasourceSelector } from './datasource_selector'; import { DatasourcePreview } from './datasource_preview'; const { DatasourceDatasourceComponent: strings } = ComponentStrings; +const { DemoData: demoDataStrings } = DataSourceStrings; export class DatasourceComponent extends PureComponent { static propTypes = { @@ -113,7 +116,13 @@ export class DatasourceComponent extends PureComponent { const { defaultIndex } = this.state; if (selecting) { - return ; + return ( + + ); } const datasourcePreview = previewing ? ( @@ -124,47 +133,51 @@ export class DatasourceComponent extends PureComponent { /> ) : null; + const datasourceRender = stateDatasource.render({ + args: stateArgs, + updateArgs, + datasourceDef, + isInvalid, + setInvalid, + defaultIndex, + }); + return ( - +
setSelecting(!selecting)} + className="canvasDataSource__triggerButton" + flush="left" + size="s" > - {strings.getChangeButtonLabel()} + + {stateDatasource.displayName} - {stateDatasource.render({ - args: stateArgs, - updateArgs, - datasourceDef, - isInvalid, - setInvalid, - defaultIndex, - })} - + {stateDatasource.name === 'demodata' ? ( + + {datasourceRender} + + ) : ( + datasourceRender + )} + - setPreviewing(true)} icon="check"> + setPreviewing(true)}> {strings.getPreviewButtonLabel()} - + - + {strings.getSaveButtonLabel()} - +
{datasourcePreview}
diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js index e6d2fe550a935..13cd2c5cd11f7 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js @@ -16,6 +16,7 @@ import { EuiPanel, EuiText, EuiEmptyPrompt, + EuiSpacer, } from '@elastic/eui'; import { Datatable } from '../../datatable'; import { Error } from '../../error'; @@ -31,21 +32,22 @@ export const DatasourcePreview = ({ done, datatable }) => ( {strings.getModalTitle()} - +

{datasourceStrings.getSaveButtonLabel()}, }} />

+ {datatable.type === 'error' ? ( ) : ( - + {datatable.rows.length > 0 ? ( ) : ( diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js index 07df2a7007c4f..92f9b92cb1f06 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js @@ -8,17 +8,21 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiCard, EuiIcon } from '@elastic/eui'; -export const DatasourceSelector = ({ onSelect, datasources }) => ( -
+export const DatasourceSelector = ({ onSelect, datasources, current }) => ( +
{datasources.map(d => ( } - onClick={() => onSelect(d.name)} + titleElement="h5" + icon={} description={d.help} layout="horizontal" className="canvasDataSource__card" + selectable={{ + isSelected: d.name === current ? true : false, + onClick: () => onSelect(d.name), + }} /> ))}
@@ -27,4 +31,5 @@ export const DatasourceSelector = ({ onSelect, datasources }) => ( DatasourceSelector.propTypes = { onSelect: PropTypes.func.isRequired, datasources: PropTypes.array.isRequired, + current: PropTypes.string.isRequired, }; diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js b/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js index caafa068c6b5b..f531ee1668aef 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js @@ -6,18 +6,17 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiPanel, EuiText } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; import { ComponentStrings } from '../../../i18n'; const { DatasourceNoDatasource: strings } = ComponentStrings; export const NoDatasource = () => ( - - -

{strings.getPanelTitle()}

+
+

{strings.getPanelDescription()}

- - +
+
); NoDatasource.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss b/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss index daccfdff5d34b..bd11bff18e091 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss +++ b/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss @@ -4,6 +4,7 @@ display: flex; flex-direction: column; justify-content: space-between; + font-size: $euiFontSizeS; .canvasDataTable__tableWrapper { @include euiScrollBar; @@ -12,7 +13,8 @@ overflow: auto; // removes white square in the scrollbar corner - &::-webkit-scrollbar-corner { // sass-lint:disable-line no-vendor-prefixes + // sass-lint:disable no-vendor-prefixes + &::-webkit-scrollbar-corner { background: transparent; } } @@ -21,6 +23,8 @@ width: 100%; display: flex; justify-content: space-around; + padding: $euiSizeS; + border-top: $euiBorderThin; } .canvasDataTable__table { @@ -30,7 +34,8 @@ .canvasDataTable__th, .canvasDataTable__td { text-align: left; - padding: $euiSizeS; + padding: $euiSizeS $euiSizeXS; + border-bottom: $euiBorderThin; } .canvasDataTable__th { diff --git a/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js b/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js index 76007994e56bf..5d710ef883548 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js +++ b/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiStat, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiAccordion, EuiText, EuiSpacer } from '@elastic/eui'; import PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; +import React from 'react'; import { ComponentStrings } from '../../../i18n'; const { ElementConfig: strings } = ComponentStrings; @@ -20,26 +20,51 @@ export const ElementConfig = ({ elementStats }) => { const progress = total > 0 ? Math.round(((ready + error) / total) * 100) : 100; return ( - - -

{strings.getTitle()}

-
- - + + {strings.getTitle()} +
+ } + initialIsOpen={false} + > + + - + - + - + - + - + ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js b/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js index 636d9b0006ac6..11c8ab88a4cba 100644 --- a/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js +++ b/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js @@ -28,6 +28,7 @@ export const ESFieldSelect = ({ value, fields = [], onChange, onFocus, onBlur }) onBlur={onBlur} singleSelection={{ asPlainText: true }} isClearable={false} + compressed /> ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js b/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js index fedb4aba7d3d0..ca2cac5a64793 100644 --- a/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js +++ b/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js @@ -25,6 +25,7 @@ export const ESFieldsSelect = ({ selected, fields, onChange, onFocus, onBlur }) className="canvasFieldsSelect" onFocus={onFocus} onBlur={onBlur} + compressed /> ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js b/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js index edc4506f20bda..8f1a4932a5e6c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js +++ b/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js @@ -32,6 +32,7 @@ export const ESIndexSelect = ({ value, loading, indices, onChange, onFocus, onBl singleSelection={{ asPlainText: true }} isClearable={false} onCreateOption={input => onChange(input || defaultIndex)} + compressed /> ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js b/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js index 2586b4ec61f04..583bf1427aab1 100644 --- a/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js +++ b/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js @@ -6,7 +6,15 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import { EuiCard, EuiFormRow, EuiTitle, EuiSpacer, EuiSelect } from '@elastic/eui'; +import { + EuiCard, + EuiFormRow, + EuiTitle, + EuiSpacer, + EuiSelect, + EuiToolTip, + EuiIcon, +} from '@elastic/eui'; import { WorkpadColorPicker } from '../workpad_color_picker'; import { ComponentStrings } from '../../../i18n'; @@ -22,14 +30,20 @@ export const PageConfig = ({ }) => { return ( - +

{strings.getTitle()}

- + + + {strings.getBackgroundColorLabel()}{' '} + + + + } > diff --git a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot index 426c07dac497e..a23452a43ae1e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot @@ -13,20 +13,24 @@ exports[`Storyshots components/Shapes/ShapePickerPopover default 1`] = `
- + +
`; @@ -44,27 +48,31 @@ exports[`Storyshots components/Shapes/ShapePickerPopover interactive 1`] = `
- + /> + +
`; @@ -82,27 +90,31 @@ exports[`Storyshots components/Shapes/ShapePickerPopover shape selected 1`] = `
- + /> + +
`; diff --git a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx index 472a14071208a..970f72da698ba 100644 --- a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx @@ -6,7 +6,7 @@ import React, { MouseEvent } from 'react'; import PropTypes from 'prop-types'; -import { EuiLink } from '@elastic/eui'; +import { EuiLink, EuiPanel } from '@elastic/eui'; import { Popover } from '../popover'; import { ShapePicker } from '../shape_picker'; import { ShapePreview } from '../shape_preview'; @@ -21,9 +21,11 @@ interface Props { export const ShapePickerPopover = ({ shapes, onChange, value }: Props) => { const button = (handleClick: (ev: MouseEvent) => void) => ( - - - + + + + + ); return ( diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot index dc80af01f121f..1655320700f87 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot @@ -2,13 +2,17 @@ exports[`Storyshots components/Sidebar/GroupSettings default 1`] = `
-

- Ungroup (U) to edit individual element settings. -

-

- Save this group as a new element to re-use it throughout your workpad. -

+
+

+ Ungroup (U) to edit individual element settings. +

+

+ Save this group as a new element to re-use it throughout your workpad. +

+
`; diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot index b9b13ae36e730..49e804640081d 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot @@ -2,13 +2,17 @@ exports[`Storyshots components/Sidebar/MultiElementSettings default 1`] = `
-

- Multiple elements are currently selected. -

-

- Deselect these elements to edit their individual settings, press (G) to group them, or save this selection as a new element to re-use it throughout your workpad. -

+
+

+ Multiple elements are currently selected. +

+

+ Deselect these elements to edit their individual settings, press (G) to group them, or save this selection as a new element to re-use it throughout your workpad. +

+
`; diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx index 6d884c05cd13a..74f4887601d30 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx @@ -6,7 +6,7 @@ import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; -import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; +import { EuiTabbedContent } from '@elastic/eui'; // @ts-ignore unconverted component import { Datasource } from '../../datasource'; // @ts-ignore unconverted component @@ -30,7 +30,6 @@ export const ElementSettings: FunctionComponent = ({ element }) => { name: strings.getDisplayTabLabel(), content: (
-
@@ -42,7 +41,6 @@ export const ElementSettings: FunctionComponent = ({ element }) => { name: strings.getDataTabLabel(), content: (
-
), diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx index a5920ee197460..2e241681ccc6a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx @@ -20,10 +20,10 @@ export const GlobalConfig: FunctionComponent = () => ( - + - + ); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx index b46465d9ec775..95d9035774a6a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx @@ -11,8 +11,10 @@ import { ComponentStrings } from '../../../i18n'; const { GroupSettings: strings } = ComponentStrings; export const GroupSettings: FunctionComponent = () => ( - -

{strings.getUngroupDescription()}

-

{strings.getSaveGroupDescription()}

-
+
+ +

{strings.getUngroupDescription()}

+

{strings.getSaveGroupDescription()}

+
+
); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx index 2de3a805c95e9..999c1c2daaf5b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx @@ -11,8 +11,10 @@ import { ComponentStrings } from '../../../i18n'; const { MultiElementSettings: strings } = ComponentStrings; export const MultiElementSettings: FunctionComponent = () => ( - -

{strings.getMultipleElementsDescription()}

-

{strings.getMultipleElementsActionsDescription()}

-
+
+ +

{strings.getMultipleElementsDescription()}

+

{strings.getMultipleElementsActionsDescription()}

+
+
); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss index f9ce6f3cfb555..338d515165e43 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss @@ -2,7 +2,6 @@ @include euiScrollBar; width: 100%; - padding: $euiSizeM; max-height: 100vh; overflow-y: auto; overflow-x: hidden; @@ -25,6 +24,15 @@ margin-bottom: $euiSizeS; } +.canvasSidebar__panel { + border-bottom: $euiBorderThin; + padding: $euiSizeS $euiSizeM; + + &--isEmpty { + border-bottom: none; + } +} + .canvasSidebar__panel-noMinWidth .euiButton { min-width: 0; } diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js index 29ca72a9737a1..bf149a6c2acb8 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js @@ -7,10 +7,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiPanel } from '@elastic/eui'; - export const SidebarSection = ({ children }) => ( - {children} +
{children}
); SidebarSection.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js index 192786ae86a45..8e1522eae8dcc 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js @@ -10,7 +10,7 @@ import { EuiTitle, EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; export const SidebarSectionTitle = ({ title, tip, children }) => { const formattedTitle = ( - +

{title}

); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss index 24453fcf0411e..92b0c50a6be4f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss @@ -1,3 +1,7 @@ .canvasLayout__sidebarHeader { - padding: ($euiSizeXS * .5) 0; + padding: calc(#{$euiSizeM} + 2px) $euiSizeS; } + +.canvasLayout__sidebarHeaderWorkpad { + padding: calc(#{$euiSizeS} * .75) 0; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js b/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js index 9693540769d50..1a44181475091 100644 --- a/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js +++ b/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js @@ -105,22 +105,30 @@ export const TextStylePicker = ({ return (
+ + doChange('family', value)} /> + doChange('size', Number(e.target.value))} options={fontSizes.map(size => ({ text: String(size), value: size }))} + prepend="Size" /> - - doChange('family', value)} /> - - + - + + + doChange('color', value)} + colors={colors} + /> + @@ -138,13 +147,7 @@ export const TextStylePicker = ({ isIconOnly idSelected={align} onChange={onAlignmentChange} - /> - - - doChange('color', value)} - colors={colors} + className="canvasSidebar__buttonGroup" /> diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js b/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js index ec7386bddace6..7dfc378432b57 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js @@ -67,9 +67,11 @@ export class WorkpadConfig extends PureComponent { return (
- -

{strings.getTitle()}

-
+
+ +

{strings.getTitle()}

+
+
@@ -129,37 +131,38 @@ export class WorkpadConfig extends PureComponent {
- - - - {strings.getGlobalCSSLabel()} - - - } - > -
- this.setState({ css: e.target.value })} - rows={10} - /> - - setWorkpadCSS(css || DEFAULT_WORKPAD_CSS)}> - {strings.getApplyStylesheetButtonLabel()} - - -
-
+
+ + + {strings.getGlobalCSSLabel()} + + + } + > +
+ this.setState({ css: e.target.value })} + rows={10} + /> + + setWorkpadCSS(css || DEFAULT_WORKPAD_CSS)}> + {strings.getApplyStylesheetButtonLabel()} + + +
+
+
); } diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot index 5525df639be01..ffe87129c76fa 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot @@ -12,7 +12,7 @@ exports[`Storyshots arguments/SeriesStyle extended 1`] = ` >
- Series Identifier + Series id
- Select Series + Select series