From f1866b331e00ebfae83ec8d02e0dfa1ab4d43ea9 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 31 Mar 2020 09:14:56 -0400 Subject: [PATCH 001/104] [ML] Settings: Increase number of items that can be paged in calendars and filters lists (#61842) * set size limit to 1000 for calendars/filters get routes * update comment * update default size comment --- x-pack/plugins/ml/server/client/elasticsearch_ml.ts | 8 ++++---- x-pack/plugins/ml/server/routes/calendars.ts | 1 + x-pack/plugins/ml/server/routes/filters.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts index ed4dc64cde3bd..caedaed92e5b1 100644 --- a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts +++ b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts @@ -514,7 +514,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) needBody: true, method: 'POST', }); - + // Currently the endpoint uses a default size of 100 unless a size is supplied. So until paging is supported in the UI, explicitly supply a size of 1000 ml.calendars = ca({ urls: [ { @@ -526,7 +526,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) }, }, { - fmt: '/_ml/calendars/', + fmt: '/_ml/calendars?size=1000', }, ], method: 'GET', @@ -671,7 +671,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) }, method: 'DELETE', }); - + // Currently the endpoint uses a default size of 100 unless a size is supplied. So until paging is supported in the UI, explicitly supply a size of 1000 ml.filters = ca({ urls: [ { @@ -683,7 +683,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) }, }, { - fmt: '/_ml/filters/', + fmt: '/_ml/filters?size=1000', }, ], method: 'GET', diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts index 63984728a18dd..34950c6ed79f7 100644 --- a/x-pack/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -42,6 +42,7 @@ function getCalendarsByIds(context: RequestHandlerContext, calendarIds: string) } export function calendars({ router, mlLicense }: RouteInitialization) { + // Gets calendars - size limit has been explicitly set to 1000 router.get( { path: '/api/ml/calendars', diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index 2f823d79a8e53..e827ed96b12af 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -47,7 +47,7 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup Filters * - * @api {get} /api/ml/filters Gets filters + * @api {get} /api/ml/filters Gets filters - size limit has been explicitly set to 1000 * @apiName GetFilters * @apiDescription Retrieves the list of filters which are used for custom rules in anomaly detection. * From 810cbd2820c25ba715c3b10aa9c4117a65f74caa Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 31 Mar 2020 16:20:37 +0300 Subject: [PATCH 002/104] [SIEM][CASE] Fix aria-labels and translations (#61670) * Fix aria-labels and translations * Fix conflicts and bugs --- .../public/components/filter_popover/index.tsx | 1 + .../link_icon/__snapshots__/index.test.tsx.snap | 1 + .../siem/public/components/link_icon/index.tsx | 14 +++++++++++++- .../siem/public/components/links/index.tsx | 13 ++++++++----- .../siem/public/components/links/translations.ts | 15 +++++++++++++++ .../components/utility_bar/utility_bar_action.tsx | 1 + .../pages/case/components/all_cases/actions.tsx | 4 ++-- .../pages/case/components/all_cases/columns.tsx | 5 ++++- .../case/components/all_cases/translations.ts | 6 ++++++ .../case/components/configure_cases/button.tsx | 1 + .../case/components/configure_cases/index.tsx | 4 ++-- .../case/components/property_actions/index.tsx | 5 ++++- .../components/property_actions/translations.ts | 11 +++++++++++ .../pages/case/components/tag_list/index.tsx | 4 ++-- .../case/components/tag_list/translations.ts | 13 +++++++++++++ .../pages/case/components/user_list/index.tsx | 11 ++++++++--- .../case/components/user_list/translations.ts | 13 +++++++++++++ 17 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/links/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_list/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx index 0c4497f7630c9..3c01ec18a879f 100644 --- a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx @@ -76,6 +76,7 @@ export const FilterPopoverComponent = ({ numFilters={options.length} hasActiveFilters={selectedOptions.length > 0} numActiveFilters={selectedOptions.length} + aria-label={buttonLabel} > {buttonLabel} diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap index c5086c8cde285..986e9161c519d 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap @@ -2,6 +2,7 @@ exports[`LinkIcon it renders 1`] = ` {children})< @@ -52,7 +53,17 @@ export interface LinkIconProps extends LinkProps { } export const LinkIcon = React.memo( - ({ children, color, disabled, href, iconSide = 'left', iconSize = 's', iconType, onClick }) => ( + ({ + children, + color, + disabled, + href, + iconSide = 'left', + iconSize = 's', + iconType, + onClick, + ariaLabel, + }) => ( ( href={href} iconSide={iconSide} onClick={onClick} + aria-label={ariaLabel ?? children} > {children} diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index 14dc5e7999a65..62a67af6e08b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -23,12 +23,13 @@ import { import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; import { useUiSetting$ } from '../../lib/kibana'; import { IP_REPUTATION_LINKS_SETTING } from '../../../common/constants'; -import * as i18n from '../page/network/ip_overview/translations'; import { isUrlInvalid } from '../../pages/detection_engine/rules/components/step_about_rule/helpers'; import { ExternalLinkIcon } from '../external_link_icon'; import { navTabs } from '../../pages/home/home_navigations'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; +import * as i18n from './translations'; + export const DEFAULT_NUMBER_OF_LINK = 5; // Internal Links @@ -88,16 +89,18 @@ const IPDetailsLinkComponent: React.FC<{ export const IPDetailsLink = React.memo(IPDetailsLinkComponent); -const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailName: string }> = ({ - children, - detailName, -}) => { +const CaseDetailsLinkComponent: React.FC<{ + children?: React.ReactNode; + detailName: string; + title?: string; +}> = ({ children, detailName, title }) => { const search = useGetUrlSearch(navTabs.case); return ( {children ? children : detailName} diff --git a/x-pack/legacy/plugins/siem/public/components/links/translations.ts b/x-pack/legacy/plugins/siem/public/components/links/translations.ts new file mode 100644 index 0000000000000..bed867cd5bf50 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/links/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../page/network/ip_overview/translations'; + +export const CASE_DETAILS_LINK_ARIA = (detailName: string) => + i18n.translate('xpack.siem.case.caseTable.caseDetailsLinkAria', { + values: { detailName }, + defaultMessage: 'click to visit case with title {detailName}', + }); diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx index 19e884e326390..330e7f83b5b28 100644 --- a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx @@ -18,6 +18,7 @@ const Popover = React.memo( return ( > => [ { - description: i18n.DELETE, + description: i18n.DELETE_CASE, icon: 'trash', - name: i18n.DELETE, + name: i18n.DELETE_CASE, onClick: deleteCaseOnClick, type: 'icon', 'data-test-subj': 'action-delete', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index f4cfac49f72e6..f757fd33a93a8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -46,7 +46,9 @@ export const getCasesColumns = ( render: (theCase: Case) => { if (theCase.id != null && theCase.title != null) { const caseDetailsLinkComponent = ( - {theCase.title} + + {theCase.title} + ); return theCase.status === 'open' ? ( caseDetailsLinkComponent @@ -184,6 +186,7 @@ const ServiceNowColumn: React.FC = ({ theCase }) => { data-test-subj={`case-table-column-external`} href={theCase.externalService?.externalUrl} target="_blank" + aria-label={i18n.SERVICENOW_LINK_ARIA} > {theCase.externalService?.externalTitle} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index e8459454576e3..1bee96bc23fff 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -59,9 +59,11 @@ export const CLOSED_CASES = i18n.translate('xpack.siem.case.caseTable.closedCase export const CLOSED = i18n.translate('xpack.siem.case.caseTable.closed', { defaultMessage: 'Closed', }); + export const DELETE = i18n.translate('xpack.siem.case.caseTable.delete', { defaultMessage: 'Delete', }); + export const REQUIRES_UPDATE = i18n.translate('xpack.siem.case.caseTable.requiresUpdate', { defaultMessage: ' requires update', }); @@ -76,3 +78,7 @@ export const NOT_PUSHED = i18n.translate('xpack.siem.case.caseTable.notPushed', export const REFRESH = i18n.translate('xpack.siem.case.caseTable.refreshTitle', { defaultMessage: 'Refresh', }); + +export const SERVICENOW_LINK_ARIA = i18n.translate('xpack.siem.case.caseTable.serviceNowLinkAria', { + defaultMessage: 'click to view the incident on servicenow', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx index 9cfc51da22e87..b0bea83148bda 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx @@ -31,6 +31,7 @@ const ConfigureCaseButtonComponent: React.FC = ({ href={getConfigureCasesUrl(urlSearch)} iconType="controlsHorizontal" isDisabled={isDisabled} + aria-label={label} > {label} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx index 241b0b1230274..b8cf5a3880801 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -298,7 +298,7 @@ const ConfigureCasesComponent: React.FC = ({ userC iconType="cross" isDisabled={isLoadingAny} isLoading={persistLoading} - aria-label="Cancel" + aria-label={i18n.CANCEL} href={getCaseUrl(search)} > {i18n.CANCEL} @@ -309,7 +309,7 @@ const ConfigureCasesComponent: React.FC = ({ userC fill color="secondary" iconType="save" - aria-label="Save" + aria-label={i18n.SAVE_CHANGES} isDisabled={isLoadingAny} isLoading={persistLoading} onClick={handleSubmit} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx index 0e7a41edace04..6b8e00921abcb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx @@ -7,6 +7,8 @@ import React, { useCallback, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPopover, EuiButtonIcon, EuiButtonEmpty } from '@elastic/eui'; +import * as i18n from './translations'; + export interface PropertyActionButtonProps { disabled?: boolean; onClick: () => void; @@ -57,10 +59,11 @@ export const PropertyActions = React.memo(({ propertyActio diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/translations.ts new file mode 100644 index 0000000000000..4d7e15a76739d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/translations.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ACTIONS_ARIA = i18n.translate('xpack.siem.case.caseView.editActionsLinkAria', { + defaultMessage: 'click to see all actions', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx index 7c456d27aceda..f7d890ca60b16 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -17,7 +17,7 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import styled, { css } from 'styled-components'; -import * as i18n from '../../translations'; +import * as i18n from './translations'; import { Form, useForm } from '../../../../shared_imports'; import { schema } from './schema'; import { CommonUseField } from '../create'; @@ -66,7 +66,7 @@ export const TagList = React.memo( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/translations.ts new file mode 100644 index 0000000000000..f7f215248dad8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/translations.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../translations'; + +export const EDIT_TAGS_ARIA = i18n.translate('xpack.siem.case.caseView.editTagsLinkAria', { + defaultMessage: 'click to edit tags', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx index 914bbe1d3f38f..579e8e48fa147 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -5,6 +5,8 @@ */ import React, { useCallback } from 'react'; +import { isEmpty } from 'lodash/fp'; + import { EuiButtonIcon, EuiText, @@ -15,8 +17,11 @@ import { EuiLoadingSpinner, EuiToolTip, } from '@elastic/eui'; + import styled, { css } from 'styled-components'; + import { ElasticUser } from '../../../../containers/case/types'; +import * as i18n from './translations'; interface UserListProps { email: { @@ -50,7 +55,7 @@ const renderUsers = ( - {fullName ?? username}

}> + {fullName ? fullName : username ?? ''}

}>

{username} @@ -65,8 +70,8 @@ const renderUsers = ( data-test-subj="user-list-email-button" onClick={handleSendEmail.bind(null, email)} iconType="email" - aria-label="email" - isDisabled={email == null} + aria-label={i18n.SEND_EMAIL_ARIA(fullName ? fullName : username ?? '')} + isDisabled={isEmpty(email)} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/translations.ts new file mode 100644 index 0000000000000..4d50d11f39101 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/translations.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SEND_EMAIL_ARIA = (user: string) => + i18n.translate('xpack.siem.case.caseView.sendEmalLinkAria', { + values: { user }, + defaultMessage: 'click to send an email to {user}', + }); From d73671e2b50f25cd165baa8b328117fcee61453f Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Tue, 31 Mar 2020 08:03:05 -0600 Subject: [PATCH 003/104] [Maps] Explicitly pass fetch function to ems-client (#61846) * Add fetchFunction binding appropriate version of fetch for ems client * Wrap standard window fetch prior to passing to ems-client Co-authored-by: Elastic Machine --- src/legacy/ui/public/vis/map/service_settings.js | 4 ++++ x-pack/legacy/plugins/maps/server/routes.js | 1 + 2 files changed, 5 insertions(+) diff --git a/src/legacy/ui/public/vis/map/service_settings.js b/src/legacy/ui/public/vis/map/service_settings.js index 9f3d21831e3da..a014aeb182c67 100644 --- a/src/legacy/ui/public/vis/map/service_settings.js +++ b/src/legacy/ui/public/vis/map/service_settings.js @@ -53,6 +53,10 @@ uiModules tileApiUrl: mapConfig.emsTileApiUrl, htmlSanitizer: $sanitize, landingPageUrl: mapConfig.emsLandingPageUrl, + // Wrap to avoid errors passing window fetch + fetchFunction: function(...args) { + return fetch(...args); + }, }); } diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js index 6aacfdc41aeea..20e022001577a 100644 --- a/x-pack/legacy/plugins/maps/server/routes.js +++ b/x-pack/legacy/plugins/maps/server/routes.js @@ -44,6 +44,7 @@ export function initRoutes(server, licenseUid) { fileApiUrl: mapConfig.emsFileApiUrl, tileApiUrl: mapConfig.emsTileApiUrl, landingPageUrl: mapConfig.emsLandingPageUrl, + fetchFunction: fetch, }); emsClient.addQueryParams({ license: licenseUid }); } else { From f29a5dd05b9ddac7d55a9f164fc5e937d9e47d8a Mon Sep 17 00:00:00 2001 From: DziyanaDzeraviankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Tue, 31 Mar 2020 17:17:09 +0300 Subject: [PATCH 004/104] =?UTF-8?q?Auto=20interval=20on=20date=20histogram?= =?UTF-8?q?=20is=20getting=20displayed=20as=20timestamp=20per=E2=80=A6=20(?= =?UTF-8?q?#59171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Auto interval on date histogram is getting displayed as timestamp per 0 milliseconds when x-axis bucket is collapsed Closes #57822 * Fixed incorrect interval label displaying while scaling the chart. * Updated agg.test.tsx snapshot * Got rid of context and refactored agg hooks * Fixed agg.type.name check * Added functional tests to cover the date histogram interval editing * Fixed some expected values in tests * Updated some test cases * Added a new visualization to visualize archive * Added testSubjects service to replace find where possible * Updated tests to match updated behavior Co-authored-by: Elastic Machine --- .../public/components/agg.tsx | 21 +++--- .../public/components/agg_group.tsx | 4 ++ .../public/components/agg_params_helper.ts | 15 +++- .../components/controls/time_interval.tsx | 4 +- .../public/components/sidebar/data_tab.tsx | 4 ++ .../public/components/sidebar/sidebar.tsx | 4 ++ .../public/default_editor.tsx | 1 + test/functional/apps/visualize/_area_chart.js | 71 +++++++++++++++++++ .../fixtures/es_archiver/visualize/data.json | 21 ++++++ 9 files changed, 132 insertions(+), 13 deletions(-) diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx index 2a45273207623..83fbf70c9099e 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx @@ -35,6 +35,8 @@ import { AGGS_ACTION_KEYS, AggsAction } from './agg_group_state'; import { RowsOrColumnsControl } from './controls/rows_or_columns'; import { RadiusRatioOptionControl } from './controls/radius_ratio_option'; import { getSchemaByName } from '../schemas'; +import { TimeRange } from '../../../../../plugins/data/public'; +import { buildAggDescription } from './agg_params_helper'; export interface DefaultEditorAggProps extends DefaultEditorAggCommonProps { agg: IAggConfig; @@ -46,6 +48,7 @@ export interface DefaultEditorAggProps extends DefaultEditorAggCommonProps { isLastBucket: boolean; isRemovable: boolean; setAggsState: React.Dispatch; + timeRange?: TimeRange; } function DefaultEditorAgg({ @@ -69,6 +72,7 @@ function DefaultEditorAgg({ removeAgg, setAggsState, schemas, + timeRange, }: DefaultEditorAggProps) { const [isEditorOpen, setIsEditorOpen] = useState((agg as any).brandNew); const [validState, setValidState] = useState(true); @@ -103,18 +107,15 @@ function DefaultEditorAgg({ } } - // A description of the aggregation, for displaying in the collapsed agg header - let aggDescription = ''; + const [aggDescription, setAggDescription] = useState(buildAggDescription(agg)); - if (agg.type && agg.type.makeLabel) { - try { - aggDescription = agg.type.makeLabel(agg); - } catch (e) { - // Date Histogram's `makeLabel` implementation invokes 'write' method for each param, including interval's 'write', - // which throws an error when interval is undefined. - aggDescription = ''; + // This useEffect is required to update the timeRange value and initiate rerender to keep labels up to date (Issue #57822). + useEffect(() => { + if (timeRange && aggName === 'date_histogram') { + agg.aggConfigs.setTimeRange(timeRange); } - } + setAggDescription(buildAggDescription(agg)); + }, [agg, aggName, timeRange]); useEffect(() => { if (isLastBucketAgg && ['date_histogram', 'histogram'].includes(aggName)) { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx index 08b69ef37f528..f50abc3ebb599 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx @@ -42,6 +42,7 @@ import { } from './agg_group_helper'; import { aggGroupReducer, initAggsState, AGGS_ACTION_KEYS } from './agg_group_state'; import { Schema, getSchemasByGroup } from '../schemas'; +import { TimeRange } from '../../../../../plugins/data/public'; export interface DefaultEditorAggGroupProps extends DefaultEditorAggCommonProps { schemas: Schema[]; @@ -49,6 +50,7 @@ export interface DefaultEditorAggGroupProps extends DefaultEditorAggCommonProps reorderAggs: ReorderAggs; setValidity(modelName: string, value: boolean): void; setTouched(isTouched: boolean): void; + timeRange?: TimeRange; } function DefaultEditorAggGroup({ @@ -67,6 +69,7 @@ function DefaultEditorAggGroup({ reorderAggs, setTouched, setValidity, + timeRange, }: DefaultEditorAggGroupProps) { const groupNameLabel = (search.aggs.aggGroupNamesMap() as any)[groupName]; // e.g. buckets can have no aggs @@ -185,6 +188,7 @@ function DefaultEditorAggGroup({ removeAgg={removeAgg} setAggsState={setAggsState} schemas={schemas} + timeRange={timeRange} /> )} diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts index 10590e1a59f4a..073cb7d5ac66c 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -174,4 +174,17 @@ function isInvalidParamsTouched( return invalidParams.every(param => param.touched); } -export { getAggParamsToRender, getAggTypeOptions, isInvalidParamsTouched }; +function buildAggDescription(agg: IAggConfig) { + let description = ''; + if (agg.type && agg.type.makeLabel) { + try { + description = agg.type.makeLabel(agg); + } catch (e) { + // Date Histogram's `makeLabel` implementation invokes 'write' method for each param, including interval's 'write', + // which throws an error when interval is undefined. + } + } + return description; +} + +export { getAggParamsToRender, getAggTypeOptions, isInvalidParamsTouched, buildAggDescription }; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx index 9aacd6a10262f..971a62faf7d7c 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx @@ -61,7 +61,7 @@ function validateInterval( timeBase?: string ) { if (definedOption) { - return { isValid: true }; + return { isValid: true, interval: agg.buckets?.getInterval() }; } if (!value) { @@ -131,7 +131,7 @@ function TimeIntervalParamEditor({ const scaledHelpText = interval && interval.scaled ? ( - + @@ -128,6 +131,7 @@ function DefaultEditorDataTab({ diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx index 29039715066be..071e10682e22c 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx @@ -33,6 +33,7 @@ import { PersistedState } from '../../../../../../plugins/visualizations/public' import { SavedSearch } from '../../../../../../plugins/discover/public'; import { AggGroupNames } from '../../../../../../plugins/data/public'; import { getSchemasByGroup } from '../../schemas'; +import { TimeRange } from '../../../../../../plugins/data/public'; interface DefaultEditorSideBarProps { isCollapsed: boolean; @@ -43,6 +44,7 @@ interface DefaultEditorSideBarProps { isLinkedSearch: boolean; eventEmitter: EventEmitter; savedSearch?: SavedSearch; + timeRange: TimeRange; } function DefaultEditorSideBar({ @@ -54,6 +56,7 @@ function DefaultEditorSideBar({ isLinkedSearch, eventEmitter, savedSearch, + timeRange, }: DefaultEditorSideBarProps) { const [selectedTab, setSelectedTab] = useState(optionTabs[0].name); const [isDirty, setDirty] = useState(false); @@ -214,6 +217,7 @@ function DefaultEditorSideBar({ ); diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx index b504dfd6a55e9..899b9c1b5fd6e 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx @@ -92,6 +92,7 @@ function DefaultEditor({ uiState={uiState} isLinkedSearch={linked} savedSearch={savedSearch} + timeRange={timeRange} eventEmitter={eventEmitter} /> diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index ebbeb01cbc917..8f2012d7f184d 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -554,5 +554,76 @@ export default function({ getService, getPageObjects }) { }); }); }); + + describe('date histogram interval', () => { + before(async () => { + await PageObjects.visualize.loadSavedVisualization('Visualization AreaChart'); + await PageObjects.visChart.waitForVisualization(); + }); + + beforeEach(async () => { + const fromTime = 'Sep 20, 2015 @ 00:00:00.000'; + const toTime = 'Sep 20, 2015 @ 23:30:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + }); + + it('should update collapsed accordion label when time range is changed', async () => { + const accordionLabel = await find.byCssSelector( + '[data-test-subj="visEditorAggAccordion2"] .visEditorSidebar__aggGroupAccordionButtonContent' + ); + let accordionLabelText = await accordionLabel.getVisibleText(); + expect(accordionLabelText).to.include.string('per 30 minutes'); + const fromTime = 'Sep 20, 2015 @ 08:30:00.000'; + const toTime = 'Sep 20, 2015 @ 23:30:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + accordionLabelText = await accordionLabel.getVisibleText(); + expect(accordionLabelText).to.include.string('per 10 minutes'); + }); + + describe('expanded accordion', () => { + before(async () => await PageObjects.visEditor.toggleAccordion('visEditorAggAccordion2')); + + it('should update label inside the opened accordion when scaled to milliseconds', async () => { + const isHelperScaledLabelExists = await find.existsByCssSelector( + '[data-test-subj="currentlyScaledText"]' + ); + expect(isHelperScaledLabelExists).to.be(false); + await PageObjects.visEditor.setInterval('Millisecond'); + const helperScaledLabelText = await testSubjects.getVisibleText('currentlyScaledText'); + expect(helperScaledLabelText).to.include.string('to 10 minutes'); + }); + + it('should display updated scaled label text after time range is changed', async () => { + await PageObjects.visEditor.setInterval('Millisecond'); + const isHelperScaledLabelExists = await find.existsByCssSelector( + '[data-test-subj="currentlyScaledText"]' + ); + expect(isHelperScaledLabelExists).to.be(true); + let helperScaledLabelText = await testSubjects.getVisibleText('currentlyScaledText'); + expect(helperScaledLabelText).to.include.string('to 10 minutes'); + const fromTime = 'Sep 20, 2015 @ 22:30:00.000'; + const toTime = 'Sep 20, 2015 @ 23:30:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + helperScaledLabelText = await testSubjects.getVisibleText('currentlyScaledText'); + expect(helperScaledLabelText).to.include.string('to 30 seconds'); + }); + + it('should update scaled label text after custom interval is set and time range is changed', async () => { + await PageObjects.visEditor.setInterval('10s', { type: 'custom' }); + await testSubjects.clickWhenNotDisabled('visualizeEditorRenderButton'); + const isHelperScaledLabelExists = await find.existsByCssSelector( + '[data-test-subj="currentlyScaledText"]' + ); + expect(isHelperScaledLabelExists).to.be(true); + let helperScaledLabelText = await testSubjects.getVisibleText('currentlyScaledText'); + expect(helperScaledLabelText).to.include.string('to 10 minutes'); + const fromTime = 'Sep 20, 2015 @ 21:30:00.000'; + const toTime = 'Sep 20, 2015 @ 23:30:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + helperScaledLabelText = await testSubjects.getVisibleText('currentlyScaledText'); + expect(helperScaledLabelText).to.include.string('to minute'); + }); + }); + }); }); } diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json index 845e9a5e08825..abca5a98bf7fd 100644 --- a/test/functional/fixtures/es_archiver/visualize/data.json +++ b/test/functional/fixtures/es_archiver/visualize/data.json @@ -69,6 +69,27 @@ } } +{ + "type": "doc", + "value": { + "id": "visualization:Visualization-AreaChart", + "index": ".kibana", + "source": { + "type": "visualization", + "visualization": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + }, + "title": "Visualization AreaChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}" + } + } + } +} + { "type": "doc", "value": { From aeba1b7bb0f11501804837bdd4dd602844cae9a4 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 31 Mar 2020 16:29:00 +0200 Subject: [PATCH 005/104] [ML] Handle Empty Partition Field Values in Single Metric Viewer (#61649) * [ML] WIP support empty partition fields values * [ML] support empty field in anomaly table * [ML] remove comments * [ML] fix context chart * [ML] rename empty field label, render as italic * [ML] rename empty field label * [ML] fix focus chart * [ML] add time range capping for fields_service.ts * [ML] empty string labels in anomaly explorer --- .../components/entity_cell/entity_cell.js | 8 ++- .../application/explorer/explorer_swimlane.js | 6 +- .../results_service/results_service.js | 36 ++---------- .../entity_control/entity_control.tsx | 38 ++++++++---- .../timeseries_search_service.ts | 2 +- .../timeseriesexplorer/timeseriesexplorer.js | 21 +++---- .../models/fields_service/fields_service.ts | 58 +++++++++++++------ 7 files changed, 96 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js index 02a9e569f28a4..d3917412bfb7b 100644 --- a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js +++ b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js @@ -10,6 +10,8 @@ import React from 'react'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EMPTY_FIELD_VALUE_LABEL } from '../../timeseriesexplorer/components/entity_control/entity_control'; +import { MLCATEGORY } from '../../../../common/constants/field_types'; function getAddFilter({ entityName, entityValue, filter }) { return ( @@ -68,7 +70,11 @@ export const EntityCell = function EntityCell({ filter, wrapText = false, }) { - const valueText = entityName !== 'mlcategory' ? entityValue : `mlcategory ${entityValue}`; + let valueText = entityValue === '' ? {EMPTY_FIELD_VALUE_LABEL} : entityValue; + if (entityName === MLCATEGORY) { + valueText = `${MLCATEGORY} ${valueText}`; + } + const textStyle = { maxWidth: '100%' }; const textWrapperClass = wrapText ? 'field-value-long' : 'field-value-short'; const shouldDisplayIcons = diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js index e8cb8377a656d..d7333f00c89cd 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js @@ -25,6 +25,7 @@ import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip import { ALLOW_CELL_RANGE_SELECTION, dragSelect$ } from './explorer_dashboard_service'; import { DRAG_SELECT_ACTION } from './explorer_constants'; import { i18n } from '@kbn/i18n'; +import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; const SCSS = { mlDragselectDragging: 'mlDragselectDragging', @@ -309,6 +310,7 @@ export class ExplorerSwimlane extends React.Component { return function(lane) { const bucketScore = getBucketScore(lane, time); if (bucketScore !== 0) { + lane = lane === '' ? EMPTY_FIELD_VALUE_LABEL : lane; cellMouseover(this, lane, bucketScore, i, time); } }; @@ -376,7 +378,7 @@ export class ExplorerSwimlane extends React.Component { values: { label: mlEscape(label) }, }); } else { - return mlEscape(label); + return label === '' ? `${EMPTY_FIELD_VALUE_LABEL}` : mlEscape(label); } }) .on('click', () => { @@ -393,7 +395,7 @@ export class ExplorerSwimlane extends React.Component { { skipHeader: true }, { label: swimlaneData.fieldName, - value, + value: value === '' ? EMPTY_FIELD_VALUE_LABEL : value, seriesIdentifier: { key: value }, valueAccessor: 'fieldName', }, diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index 4dec066a7f325..b7aa5edc88638 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -1259,39 +1259,13 @@ export function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, lates }, { term: { job_id: jobId } }, ]; - const shouldCriteria = []; _.each(criteriaFields, criteria => { - if (criteria.fieldValue.length !== 0) { - mustCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue, - }, - }); - } else { - // Add special handling for blank entity field values, checking for either - // an empty string or the field not existing. - const emptyFieldCondition = { - bool: { - must: [ - { - term: {}, - }, - ], - }, - }; - emptyFieldCondition.bool.must[0].term[criteria.fieldName] = ''; - shouldCriteria.push(emptyFieldCondition); - shouldCriteria.push({ - bool: { - must_not: [ - { - exists: { field: criteria.fieldName }, - }, - ], - }, - }); - } + mustCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue, + }, + }); }); ml.esSearch({ diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index 8911ed53e74d0..7bb0b27472c88 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -16,6 +16,7 @@ import { EuiFormRow, EuiToolTip, } from '@elastic/eui'; +import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option'; export interface Entity { fieldName: string; @@ -29,15 +30,22 @@ interface EntityControlProps { isLoading: boolean; onSearchChange: (entity: Entity, queryTerm: string) => void; forceSelection: boolean; - options: EuiComboBoxOptionOption[]; + options: Array>; } interface EntityControlState { - selectedOptions: EuiComboBoxOptionOption[] | undefined; + selectedOptions: Array> | undefined; isLoading: boolean; - options: EuiComboBoxOptionOption[] | undefined; + options: Array> | undefined; } +export const EMPTY_FIELD_VALUE_LABEL = i18n.translate( + 'xpack.ml.timeSeriesExplorer.emptyPartitionFieldLabel.', + { + defaultMessage: '"" (empty string)', + } +); + export class EntityControl extends Component { inputRef: any; @@ -53,16 +61,18 @@ export class EntityControl extends Component> | undefined = selectedOptions; if ( - (selectedOptions === undefined && fieldValue.length > 0) || + (selectedOptions === undefined && fieldValue !== null) || (Array.isArray(selectedOptions) && // @ts-ignore - selectedOptions[0].label !== fieldValue && - fieldValue.length > 0) + selectedOptions[0].value !== fieldValue && + fieldValue !== null) ) { - selectedOptionsUpdate = [{ label: fieldValue }]; - } else if (Array.isArray(selectedOptions) && fieldValue.length === 0) { + selectedOptionsUpdate = [ + { label: fieldValue === '' ? EMPTY_FIELD_VALUE_LABEL : fieldValue, value: fieldValue }, + ]; + } else if (Array.isArray(selectedOptions) && fieldValue === null) { selectedOptionsUpdate = undefined; } @@ -84,14 +94,14 @@ export class EntityControl extends Component { + onChange = (selectedOptions: Array>) => { const options = selectedOptions.length > 0 ? selectedOptions : undefined; this.setState({ selectedOptions: options, }); const fieldValue = - Array.isArray(options) && options[0].label.length > 0 ? options[0].label : ''; + Array.isArray(options) && options[0].value !== null ? options[0].value : null; this.props.entityFieldValueChanged(this.props.entity, fieldValue); }; @@ -103,6 +113,11 @@ export class EntityControl extends Component { + const { label } = option; + return label === EMPTY_FIELD_VALUE_LABEL ? {label} : label; + }; + render() { const { entity, forceSelection } = this.props; const { isLoading, options, selectedOptions } = this.state; @@ -126,6 +141,7 @@ export class EntityControl extends Component ); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index db5ff2ad91910..f973d41ad7754 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -128,7 +128,7 @@ function getChartDetails( obj.results.functionLabel = functionLabel; const blankEntityFields = _.filter(entityFields, entity => { - return entity.fieldValue.length === 0; + return entity.fieldValue === null; }); // Look to see if any of the entity fields have defined values diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 1a26540709f34..5e505757dd2aa 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -78,6 +78,7 @@ import { processRecordScoreResults, getFocusData, } from './timeseriesexplorer_utils'; +import { EMPTY_FIELD_VALUE_LABEL } from './components/entity_control/entity_control'; // Used to indicate the chart is being plotted across // all partition field values, where the cardinality of the field cannot be @@ -94,7 +95,7 @@ function getEntityControlOptions(fieldValues) { fieldValues.sort(); return fieldValues.map(value => { - return { label: value }; + return { label: value === '' ? EMPTY_FIELD_VALUE_LABEL : value, value }; }); } @@ -192,7 +193,7 @@ export class TimeSeriesExplorer extends React.Component { getFieldNamesWithEmptyValues = () => { const latestEntityControls = this.getControlsForDetector(); return latestEntityControls - .filter(({ fieldValue }) => !fieldValue) + .filter(({ fieldValue }) => fieldValue === null) .map(({ fieldName }) => fieldName); }; @@ -249,7 +250,7 @@ export class TimeSeriesExplorer extends React.Component { if (operator === '+' && entity.fieldValue !== value) { resultValue = value; } else if (operator === '-' && entity.fieldValue === value) { - resultValue = ''; + resultValue = null; } else { return; } @@ -302,7 +303,7 @@ export class TimeSeriesExplorer extends React.Component { focusAggregationInterval, selectedForecastId, modelPlotEnabled, - entityControls.filter(entity => entity.fieldValue.length > 0), + entityControls.filter(entity => entity.fieldValue !== null), searchBounds, selectedJob, TIME_FIELD_NAME @@ -576,7 +577,7 @@ export class TimeSeriesExplorer extends React.Component { }; const nonBlankEntities = entityControls.filter(entity => { - return entity.fieldValue.length > 0; + return entity.fieldValue !== null; }); if ( @@ -739,7 +740,7 @@ export class TimeSeriesExplorer extends React.Component { const overFieldName = get(detector, 'over_field_name'); const byFieldName = get(detector, 'by_field_name'); if (partitionFieldName !== undefined) { - const partitionFieldValue = get(entitiesState, partitionFieldName, ''); + const partitionFieldValue = get(entitiesState, partitionFieldName, null); entities.push({ fieldType: 'partition_field', fieldName: partitionFieldName, @@ -747,7 +748,7 @@ export class TimeSeriesExplorer extends React.Component { }); } if (overFieldName !== undefined) { - const overFieldValue = get(entitiesState, overFieldName, ''); + const overFieldValue = get(entitiesState, overFieldName, null); entities.push({ fieldType: 'over_field', fieldName: overFieldName, @@ -761,7 +762,7 @@ export class TimeSeriesExplorer extends React.Component { // TODO - metric data can be filtered by this field, so should only exclude // from filter for the anomaly records. if (byFieldName !== undefined && overFieldName === undefined) { - const byFieldValue = get(entitiesState, byFieldName, ''); + const byFieldValue = get(entitiesState, byFieldName, null); entities.push({ fieldType: 'by_field', fieldName: byFieldName, fieldValue: byFieldValue }); } @@ -775,7 +776,7 @@ export class TimeSeriesExplorer extends React.Component { */ getCriteriaFields(detectorIndex, entities) { // Only filter on the entity if the field has a value. - const nonBlankEntities = entities.filter(entity => entity.fieldValue.length > 0); + const nonBlankEntities = entities.filter(entity => entity.fieldValue !== null); return [ { fieldName: 'detector_index', @@ -1150,7 +1151,7 @@ export class TimeSeriesExplorer extends React.Component { {entityControls.map(entity => { const entityKey = `${entity.fieldName}`; - const forceSelection = !hasEmptyFieldValues && !entity.fieldValue; + const forceSelection = !hasEmptyFieldValues && entity.fieldValue === null; hasEmptyFieldValues = !hasEmptyFieldValues && forceSelection; return ( { if (!interval) { - throw new Error('Interval is required to retrieve max bucket cardinalities.'); + throw Boom.badRequest('Interval is required to retrieve max bucket cardinalities.'); } const aggregatableFields = await getAggregatableFields(index, fieldNames); @@ -243,12 +260,17 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) { return {}; } + const { start, end } = getSafeTimeRangeForInterval( + interval, + ...Object.values(getSafeTimeRange(earliestMs, latestMs)) + ); + const cachedValues = fieldsAggsCache.getValues( index, timeFieldName, - earliestMs, - latestMs, + start, + end, 'maxBucketCardinality', fieldNames ) ?? {}; @@ -260,8 +282,6 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) { return cachedValues; } - const { start, end } = getSafeTimeRange(earliestMs, latestMs, interval); - const mustCriteria = [ { range: { @@ -334,6 +354,10 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) { return obj; }, {} as { [field: string]: number }); + fieldsAggsCache.updateValues(index, timeFieldName, start, end, { + maxBucketCardinality: aggResult, + }); + return { ...cachedValues, ...aggResult, From e8143918e6c51cf09c49e9a0591c110540103bda Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 31 Mar 2020 10:42:50 -0400 Subject: [PATCH 006/104] Add an onBlur handler for the kuery bar. Only resubmit when input changes. (#61901) --- .../components/functional/kuery_bar/typeahead/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/index.js b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/index.js index 9b8bdb50441c9..0ea5e1e2b8372 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/index.js +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/typeahead/index.js @@ -27,6 +27,7 @@ export class Typeahead extends Component { index: null, value: '', inputIsPristine: true, + lastSubmitted: '', }; static getDerivedStateFromProps(props, state) { @@ -151,7 +152,10 @@ export class Typeahead extends Component { }; onSubmit = () => { - this.props.onSubmit(this.state.value); + if (this.state.lastSubmitted !== this.state.value) { + this.props.onSubmit(this.state.value); + this.setState({ lastSubmitted: this.state.value }); + } this.setState({ isSuggestionsVisible: false }); }; @@ -177,6 +181,7 @@ export class Typeahead extends Component { value={this.state.value} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} + onBlur={this.onSubmit} onChange={this.onChangeInputValue} onClick={this.onClickInput} autoComplete="off" From 4899aaf5658970ceca982975b079a37d20555985 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Tue, 31 Mar 2020 11:01:06 -0400 Subject: [PATCH 007/104] Endpoint: Add ts-node dev dependency (#61884) Co-authored-by: Elastic Machine Used to run CLI scripts written in Typescript --- x-pack/plugins/endpoint/package.json | 3 ++- x-pack/plugins/endpoint/scripts/README.md | 6 +----- yarn.lock | 26 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/endpoint/package.json b/x-pack/plugins/endpoint/package.json index fc4f4bd586bef..9e65f23a38860 100644 --- a/x-pack/plugins/endpoint/package.json +++ b/x-pack/plugins/endpoint/package.json @@ -13,6 +13,7 @@ "devDependencies": { "@types/seedrandom": ">=2.0.0 <4.0.0", "@types/react-redux": "^7.1.0", - "redux-devtools-extension": "^2.13.8" + "redux-devtools-extension": "^2.13.8", + "ts-node": "^8.8.1" } } diff --git a/x-pack/plugins/endpoint/scripts/README.md b/x-pack/plugins/endpoint/scripts/README.md index f0c8c5a9b0b66..34d0a1ecd8ede 100644 --- a/x-pack/plugins/endpoint/scripts/README.md +++ b/x-pack/plugins/endpoint/scripts/README.md @@ -3,11 +3,7 @@ The default behavior is to create 1 endpoint with 1 alert and a moderate number A seed value can be provided as a string for the random number generator for repeatable behavior, useful for demos etc. Use the `-d` option if you want to delete and remake the indices, otherwise it will add documents to existing indices. -The sample data generator script depends on ts-node, install with npm: - -```npm install -g ts-node``` - -Example command sequence to get ES and kibana running with sample data after installing ts-node: +Example command sequence to get ES and kibana running with sample data: ```yarn es snapshot``` -> starts ES diff --git a/yarn.lock b/yarn.lock index b3c2aa94d07d1..aa96a740cb3a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6103,6 +6103,11 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7, argparse@~1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -19550,6 +19555,11 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + make-iterator@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.0.tgz#57bef5dc85d23923ba23767324d8e8f8f3d9694b" @@ -28788,6 +28798,17 @@ ts-log@2.1.4: resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.1.4.tgz#063c5ad1cbab5d49d258d18015963489fb6fb59a" integrity sha512-P1EJSoyV+N3bR/IWFeAqXzKPZwHpnLY6j7j58mAvewHRipo+BQM2Y1f9Y9BjEQznKwgqqZm7H8iuixmssU7tYQ== +ts-node@^8.8.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.1.tgz#7c4d3e9ed33aa703b64b28d7f9d194768be5064d" + integrity sha512-10DE9ONho06QORKAaCBpPiFCdW+tZJuY/84tyypGtl6r+/C7Asq0dhqbRZURuUlLQtZxxDvT8eoj8cGW0ha6Bg== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "3.1.1" + ts-pnp@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.4.tgz#ae27126960ebaefb874c6d7fa4729729ab200d90" @@ -31990,6 +32011,11 @@ yeoman-generator@1.1.1: user-home "^2.0.0" yeoman-environment "^1.1.0" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yo@2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/yo/-/yo-2.0.6.tgz#7b562f68a0434237c24a1fd3982f235035839516" From 0082ca7ad8a7af7dad79d6912e6de5ad6db3b075 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 31 Mar 2020 17:01:44 +0200 Subject: [PATCH 008/104] [Discover] Deangularize and euificate sidebar (#47559) * Split angular templates into React components * Add tooltip for field label * Adapt SCSS * Cleanup angular directives * Extract helper functions * Improve tests + docs * Move css to _sidebar.scss * Exclude _id field from displaying the Visualize button to prevent an ES error * A11y improvements --- .../__tests__/directives/css_truncate.js | 81 ----- .../__tests__/directives/discover_field.js | 97 ------ .../__tests__/directives/field_chooser.js | 257 -------------- .../discover/__tests__/doc_table/doc_table.js | 1 + .../public/discover/get_inner_angular.ts | 22 +- .../kibana/public/discover/kibana_services.ts | 2 - .../public/discover/np_ready/_discover.scss | 121 ------- .../angular/directives/css_truncate.ts | 61 ---- .../discover/np_ready/angular/discover.html | 13 +- .../np_ready/angular/discover_state.ts | 2 +- .../discover/np_ready/components/_index.scss | 2 +- .../field_chooser/_field_chooser.scss | 36 -- .../components/field_chooser/_index.scss | 1 - .../field_chooser/discover_field.html | 26 -- .../field_chooser/discover_field.js | 139 -------- .../field_chooser/field_chooser.html | 99 ------ .../components/field_chooser/field_chooser.js | 296 ---------------- .../lib/detail_views/string.html | 106 ------ .../field_chooser/lib/visualize_url_utils.ts | 110 ------ .../discover_index_pattern.test.tsx.snap | 0 .../np_ready/components/sidebar/_index.scss | 1 + .../np_ready/components/sidebar/_sidebar.scss | 155 +++++++++ .../change_indexpattern.tsx | 0 .../sidebar/discover_field.test.tsx | 111 ++++++ .../components/sidebar/discover_field.tsx | 186 ++++++++++ .../sidebar/discover_field_bucket.tsx | 99 ++++++ .../sidebar/discover_field_details.tsx | 98 ++++++ .../discover_field_search.test.tsx | 3 +- .../discover_field_search.tsx | 2 +- .../discover_index_pattern.test.tsx | 0 .../discover_index_pattern.tsx | 6 +- .../sidebar/discover_sidebar.test.tsx | 131 +++++++ .../components/sidebar/discover_sidebar.tsx | 326 ++++++++++++++++++ .../discover_sidebar_directive.ts} | 25 +- .../index.ts} | 11 +- .../lib/field_calculator.js | 0 .../sidebar/lib/field_calculator.test.ts} | 126 +++---- .../sidebar/lib/field_filter.test.ts | 96 ++++++ .../components/sidebar/lib/field_filter.ts | 78 +++++ .../components/sidebar/lib/get_details.ts | 52 +++ .../sidebar/lib/get_field_type_name.ts | 73 ++++ .../lib/get_index_pattern_field_list.ts | 44 +++ .../sidebar/lib/get_warnings.ts} | 38 +- .../sidebar/lib/group_fields.test.ts | 114 ++++++ .../components/sidebar/lib/group_fields.tsx | 78 +++++ .../sidebar/lib/visualize_url_utils.ts | 188 ++++++++++ .../string_progress_bar.tsx | 11 +- .../{field_chooser => sidebar}/types.ts | 15 + .../np_ready/embeddable/search_embeddable.ts | 4 +- .../styles/_legacy/components/_index.scss | 1 - .../styles/_legacy/components/_sidebar.scss | 127 ------- .../__snapshots__/field_name.test.tsx.snap | 6 +- .../components/field_name/field_name.test.tsx | 18 +- .../components/field_name/field_name.tsx | 43 +-- .../public/components/table/table.tsx | 2 +- .../public/components/table/table_row.tsx | 4 +- test/accessibility/apps/discover.ts | 4 +- .../apps/management/_scripted_fields.js | 2 +- test/functional/page_objects/discover_page.ts | 13 +- .../common/layouts/preserve_layout.css | 2 +- .../export_types/common/layouts/print.css | 2 +- .../translations/translations/ja-JP.json | 7 - .../translations/translations/zh-CN.json | 7 - 63 files changed, 2006 insertions(+), 1775 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/discover/__tests__/directives/css_truncate.js delete mode 100644 src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js delete mode 100644 src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/css_truncate.ts delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_index.scss delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_field.html delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_field.js delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.html delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts rename src/legacy/core_plugins/kibana/public/discover/np_ready/components/{field_chooser => sidebar}/__snapshots__/discover_index_pattern.test.tsx.snap (100%) create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/_index.scss create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/_sidebar.scss rename src/legacy/core_plugins/kibana/public/discover/np_ready/components/{field_chooser => sidebar}/change_indexpattern.tsx (100%) create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field.test.tsx create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field.tsx create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field_bucket.tsx create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field_details.tsx rename src/legacy/core_plugins/kibana/public/discover/np_ready/components/{field_chooser => sidebar}/discover_field_search.test.tsx (98%) rename src/legacy/core_plugins/kibana/public/discover/np_ready/components/{field_chooser => sidebar}/discover_field_search.tsx (99%) rename src/legacy/core_plugins/kibana/public/discover/np_ready/components/{field_chooser => sidebar}/discover_index_pattern.test.tsx (100%) rename src/legacy/core_plugins/kibana/public/discover/np_ready/components/{field_chooser => sidebar}/discover_index_pattern.tsx (94%) create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.test.tsx create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.tsx rename src/legacy/core_plugins/kibana/public/discover/np_ready/components/{field_chooser/discover_index_pattern_directive.tsx => sidebar/discover_sidebar_directive.ts} (61%) rename src/legacy/core_plugins/kibana/public/discover/np_ready/components/{field_chooser/discover_field_search_directive.ts => sidebar/index.ts} (67%) rename src/legacy/core_plugins/kibana/public/discover/np_ready/components/{field_chooser => sidebar}/lib/field_calculator.js (100%) rename src/legacy/core_plugins/kibana/public/discover/{__tests__/directives/field_calculator.js => np_ready/components/sidebar/lib/field_calculator.test.ts} (64%) create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/field_filter.test.ts create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/field_filter.ts create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/get_details.ts create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/get_field_type_name.ts create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/get_index_pattern_field_list.ts rename src/legacy/core_plugins/kibana/public/discover/np_ready/{angular/directives/field_name.js => components/sidebar/lib/get_warnings.ts} (55%) create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/group_fields.test.ts create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/group_fields.tsx create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/visualize_url_utils.ts rename src/legacy/core_plugins/kibana/public/discover/np_ready/components/{field_chooser => sidebar}/string_progress_bar.tsx (80%) rename src/legacy/core_plugins/kibana/public/discover/np_ready/components/{field_chooser => sidebar}/types.ts (78%) delete mode 100644 src/legacy/ui/public/styles/_legacy/components/_sidebar.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/css_truncate.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/css_truncate.js deleted file mode 100644 index 8dea9c61475db..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/css_truncate.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import angular from 'angular'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/legacy'; - -let $parentScope; - -let $scope; - -let $elem; - -const init = function(expandable) { - // Load the application - pluginInstance.initializeServices(); - pluginInstance.initializeInnerAngular(); - ngMock.module('app/discover'); - - // Create the scope - ngMock.inject(function($rootScope, $compile) { - // Give us a scope - $parentScope = $rootScope; - - // Create the element - $elem = angular.element( - 'this isnt important' - ); - - // And compile it - $compile($elem)($parentScope); - - // Fire a digest cycle - $elem.scope().$digest(); - - // Grab the isolate scope so we can test it - $scope = $elem.isolateScope(); - }); -}; - -describe('cssTruncate directive', function() { - describe('expandable', function() { - beforeEach(function() { - init(true); - }); - - it('should set text-overflow to ellipsis and whitespace to nowrap', function(done) { - expect($elem.css('text-overflow')).to.be('ellipsis'); - expect($elem.css('white-space')).to.be('nowrap'); - done(); - }); - - it('should set white-space to normal when clicked, and back to nowrap when clicked again', function(done) { - $scope.toggle(); - expect($elem.css('white-space')).to.be('normal'); - - $scope.toggle(); - expect($elem.css('white-space')).to.be('nowrap'); - done(); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js deleted file mode 100644 index 6ffda87ac2be8..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import angular from 'angular'; -import _ from 'lodash'; -import sinon from 'sinon'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import { pluginInstance } from 'plugins/kibana/discover/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -// Load the kibana app dependencies. - -describe('discoverField', function() { - let $scope; - let indexPattern; - let $elem; - beforeEach(() => pluginInstance.initializeServices()); - beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(ngMock.module('app/discover')); - beforeEach( - ngMock.inject(function(Private, $rootScope, $compile) { - $elem = angular.element(` - - `); - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - - _.assign($rootScope, { - field: indexPattern.fields.getByName('extension'), - addField: sinon.spy(() => ($rootScope.field.display = true)), - removeField: sinon.spy(() => ($rootScope.field.display = false)), - showDetails: sinon.spy(() => ($rootScope.field.details = { exists: true })), - }); - - $compile($elem)($rootScope); - - $scope = $elem.isolateScope(); - $scope.$digest(); - sinon.spy($scope, 'toggleDetails'); - }) - ); - - afterEach(function() { - $scope.toggleDetails.restore(); - $scope.$destroy(); - }); - - describe('toggleDisplay', function() { - it('should exist', function() { - expect($scope.toggleDisplay).to.be.a(Function); - }); - - it('should call onAddField or onRemoveField depending on the display state', function() { - $scope.toggleDisplay($scope.field); - expect($scope.onAddField.callCount).to.be(1); - expect($scope.onAddField.firstCall.args).to.eql([$scope.field.name]); - - $scope.toggleDisplay($scope.field); - expect($scope.onRemoveField.callCount).to.be(1); - expect($scope.onRemoveField.firstCall.args).to.eql([$scope.field.name]); - }); - - it('should call toggleDetails when currently showing the details', function() { - $scope.toggleDetails($scope.field); - $scope.toggleDisplay($scope.field); - expect($scope.toggleDetails.callCount).to.be(2); - }); - }); - - describe('toggleDetails', function() { - it('should notify the parent when showing the details', function() { - $scope.toggleDetails($scope.field); - expect($scope.onShowDetails.callCount).to.be(1); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js deleted file mode 100644 index 47392c541890e..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import angular from 'angular'; -import ngMock from 'ng_mock'; -import _ from 'lodash'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import $ from 'jquery'; -import { pluginInstance } from 'plugins/kibana/discover/legacy'; -import FixturesHitsProvider from 'fixtures/hits'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import { SimpleSavedObject } from '../../../../../../../core/public'; - -// Load the kibana app dependencies. - -let $parentScope; -let $scope; -let hits; -let indexPattern; -let indexPatternList; - -// Sets up the directive, take an element, and a list of properties to attach to the parent scope. -const init = function($elem, props) { - ngMock.inject(function($rootScope, $compile, $timeout) { - $parentScope = $rootScope; - _.assign($parentScope, props); - $compile($elem)($parentScope); - - // Required for test to run solo. Sigh - $timeout(() => $elem.scope().$digest(), 0); - - $scope = $elem.isolateScope(); - }); -}; - -const destroy = function() { - $scope.$destroy(); - $parentScope.$destroy(); -}; - -describe('discover field chooser directives', function() { - const $elem = angular.element(` - - `); - beforeEach(() => pluginInstance.initializeServices()); - beforeEach(() => pluginInstance.initializeInnerAngular()); - - beforeEach(ngMock.module('app/discover')); - - beforeEach( - ngMock.inject(function(Private) { - hits = Private(FixturesHitsProvider); - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - indexPatternList = [ - new SimpleSavedObject(undefined, { id: '0', attributes: { title: 'b' } }), - new SimpleSavedObject(undefined, { id: '1', attributes: { title: 'a' } }), - new SimpleSavedObject(undefined, { id: '2', attributes: { title: 'c' } }), - ]; - - const fieldCounts = _.transform( - hits, - function(counts, hit) { - _.keys(indexPattern.flattenHit(hit)).forEach(function(key) { - counts[key] = (counts[key] || 0) + 1; - }); - }, - {} - ); - - init($elem, { - columns: [], - toggle: sinon.spy(), - hits: hits, - fieldCounts: fieldCounts, - addField: sinon.spy(), - addFilter: sinon.spy(), - indexPattern: indexPattern, - indexPatternList: indexPatternList, - removeField: sinon.spy(), - }); - - $scope.$digest(); - }) - ); - - afterEach(() => destroy()); - - const getSections = function(ctx) { - return { - selected: $('.dscFieldList--selected', ctx), - popular: $('.dscFieldList--popular', ctx), - unpopular: $('.dscFieldList--unpopular', ctx), - }; - }; - - describe('Field listing', function() { - it('should have Selected Fields, Fields and Popular Fields sections', function() { - const headers = $elem.find('.sidebar-list-header'); - expect(headers.length).to.be(3); - }); - - it('should have 2 popular fields, 1 unpopular field and no selected fields', function() { - const section = getSections($elem); - const popular = find('popular'); - const unpopular = find('unpopular'); - - expect(section.selected.find('li').length).to.be(0); - - expect(popular).to.contain('ssl'); - expect(popular).to.contain('@timestamp'); - expect(popular).to.not.contain('ip\n'); - - expect(unpopular).to.contain('extension'); - expect(unpopular).to.contain('machine.os'); - expect(unpopular).to.not.contain('ssl'); - - function find(popularity) { - return section[popularity] - .find('.dscFieldName') - .map((i, el) => $(el).text()) - .toArray(); - } - }); - - it('should show the popular fields header if there are popular fields', function() { - const section = getSections($elem); - expect(section.popular.hasClass('ng-hide')).to.be(false); - expect(section.popular.find('li:not(.sidebar-list-header)').length).to.be.above(0); - }); - - it('should not show the popular fields if there are not any', function() { - // Re-init - destroy(); - - _.each(indexPattern.fields, function(field) { - field.$$spec.count = 0; - }); // Reset the popular fields - init($elem, { - columns: [], - toggle: sinon.spy(), - hits: require('fixtures/hits'), - filter: sinon.spy(), - indexPattern: indexPattern, - }); - - const section = getSections($elem); - - $scope.$digest(); - expect(section.popular.hasClass('ng-hide')).to.be(true); - expect(section.popular.find('li:not(.sidebar-list-header)').length).to.be(0); - }); - - it('should move the field into selected when it is added to the columns array', function() { - const section = getSections($elem); - $scope.columns.push('bytes'); - $scope.$digest(); - - expect(section.selected.text()).to.contain('bytes'); - expect(section.popular.text()).to.not.contain('bytes'); - - $scope.columns.push('ip'); - $scope.$digest(); - expect(section.selected.text()).to.contain('ip\n'); - expect(section.unpopular.text()).to.not.contain('ip\n'); - - expect(section.popular.text()).to.contain('ssl'); - }); - }); - - describe('details processing', function() { - let field; - function getField() { - return _.find($scope.fields, { name: 'bytes' }); - } - - beforeEach(function() { - field = getField(); - }); - - it('should have a computeDetails function', function() { - expect($scope.computeDetails).to.be.a(Function); - }); - - it('should increase the field popularity when called', function() { - indexPattern.popularizeField = sinon.spy(); - $scope.computeDetails(field); - expect(indexPattern.popularizeField.called).to.be(true); - }); - - it('should append a details object to the field', function() { - $scope.computeDetails(field); - expect(field.details).to.not.be(undefined); - }); - - it('should delete the field details if they already exist', function() { - $scope.computeDetails(field); - expect(field.details).to.not.be(undefined); - $scope.computeDetails(field); - expect(field.details).to.be(undefined); - }); - - it('... unless recompute is true', function() { - $scope.computeDetails(field); - expect(field.details).to.not.be(undefined); - $scope.computeDetails(field, true); - expect(field.details).to.not.be(undefined); - }); - - it('should create buckets with formatted and raw values', function() { - $scope.computeDetails(field); - expect(field.details.buckets).to.not.be(undefined); - expect(field.details.buckets[0].value).to.be(40.141592); - }); - - it('should recalculate the details on open fields if the hits change', function() { - $scope.hits = [{ _source: { bytes: 1024 } }]; - $scope.$apply(); - - field = getField(); - $scope.computeDetails(field); - expect(getField().details.total).to.be(1); - - $scope.hits = [{ _source: { notbytes: 1024 } }]; - $scope.$apply(); - field = getField(); - expect(field.details).to.not.have.property('total'); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js index 6b97da79fc589..9e74df08233da 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js @@ -61,6 +61,7 @@ const destroy = function() { describe('docTable', function() { let $elem; beforeEach(() => pluginInstance.initializeInnerAngular()); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); beforeEach(function() { $elem = angular.element(` diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index 031e10e99289f..607d79b81618e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -27,7 +27,7 @@ import { CoreStart, LegacyCoreStart } from 'kibana/public'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; -import { createDocTableDirective } from './np_ready/angular/doc_table/doc_table'; +import { createDocTableDirective } from './np_ready/angular/doc_table'; import { createTableHeaderDirective } from './np_ready/angular/doc_table/components/table_header'; import { createToolBarPagerButtonsDirective, @@ -37,18 +37,8 @@ import { createTableRowDirective } from './np_ready/angular/doc_table/components import { createPagerFactory } from './np_ready/angular/doc_table/lib/pager/pager_factory'; import { createInfiniteScrollDirective } from './np_ready/angular/doc_table/infinite_scroll'; import { createDocViewerDirective } from './np_ready/angular/doc_viewer'; -import { createFieldSearchDirective } from './np_ready/components/field_chooser/discover_field_search_directive'; -import { createIndexPatternSelectDirective } from './np_ready/components/field_chooser/discover_index_pattern_directive'; -import { createStringFieldProgressBarDirective } from './np_ready/components/field_chooser/string_progress_bar'; -// @ts-ignore -import { FieldNameDirectiveProvider } from './np_ready/angular/directives/field_name'; -// @ts-ignore -import { createFieldChooserDirective } from './np_ready/components/field_chooser/field_chooser'; -// @ts-ignore -import { createDiscoverFieldDirective } from './np_ready/components/field_chooser/discover_field'; import { CollapsibleSidebarProvider } from './np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar'; import { DiscoverStartPlugins } from './plugin'; -import { createCssTruncateDirective } from './np_ready/angular/directives/css_truncate'; // @ts-ignore import { FixedScrollProvider } from './np_ready/angular/directives/fixed_scroll'; // @ts-ignore @@ -65,6 +55,7 @@ import { createTopNavDirective, createTopNavHelper, } from '../../../../../plugins/kibana_legacy/public'; +import { createDiscoverSidebarDirective } from './np_ready/components/sidebar'; /** * returns the main inner angular module, it contains all the parts of Angular Discover @@ -125,7 +116,6 @@ export function initializeInnerAngularModule( ]) .config(watchMultiDecorator) .directive('icon', reactDirective => reactDirective(EuiIcon)) - .directive('fieldName', FieldNameDirectiveProvider) .directive('renderComplete', createRenderCompleteDirective) .service('debounce', ['$timeout', DebounceProviderTimeout]); } @@ -149,16 +139,10 @@ export function initializeInnerAngularModule( .run(registerListenEventListener) .directive('icon', reactDirective => reactDirective(EuiIcon)) .directive('kbnAccessibleClick', KbnAccessibleClickProvider) - .directive('fieldName', FieldNameDirectiveProvider) .directive('collapsibleSidebar', CollapsibleSidebarProvider) - .directive('cssTruncate', createCssTruncateDirective) .directive('fixedScroll', FixedScrollProvider) .directive('renderComplete', createRenderCompleteDirective) - .directive('discoverFieldSearch', createFieldSearchDirective) - .directive('discoverIndexPatternSelect', createIndexPatternSelectDirective) - .directive('stringFieldProgressBar', createStringFieldProgressBarDirective) - .directive('discoverField', createDiscoverFieldDirective) - .directive('discFieldChooser', createFieldChooserDirective) + .directive('discoverSidebar', createDiscoverSidebarDirective) .service('debounce', ['$timeout', DebounceProviderTimeout]); } diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 2ceb06f325a9e..55f369eaecd2c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import angular from 'angular'; // just used in embeddables and discover controller import { DiscoverServices } from './build_services'; let angularModule: any = null; @@ -52,7 +51,6 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ }>('urlTracker'); // EXPORT legacy static dependencies, should be migrated when available in a new version; -export { angular }; export { wrapInI18nContext } from 'ui/i18n'; import { search } from '../../../../../plugins/data/public'; import { createGetterSetter } from '../../../../../plugins/kibana_utils/common'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss index 62e7a96ed80cf..8eaa66cf58624 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss @@ -1,9 +1,5 @@ discover-app { flex-grow: 1; - - .sidebar-container { - background-color: transparent; - } } .dscHistogram { @@ -12,22 +8,6 @@ discover-app { padding: $euiSizeS; } -// SASSTODO: replace the margin-top value with a variable -.dscSidebar__listHeader { - margin-top: 5px; -} - -.dscFieldList--popular { - padding-top: $euiSizeS; -} - -.dscFieldList--selected, -.dscFieldList--unpopular, -.dscFieldList--popular { - padding-left: $euiSizeS; - padding-right: $euiSizeS; -} - // SASSTODO: replace the z-index value with a variable .dscWrapper { padding-right: $euiSizeS; @@ -109,107 +89,6 @@ discover-app { text-align: center; } -/** - * 1. Override sidebar-item-title styles. - */ -.dscSidebarItem { - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - padding-top: 0 !important; /* 1 */ - padding-bottom: 0 !important; /* 1 */ - height: $euiSizeXL; - - &:hover, - &:focus { - .dscSidebarItem__action { - opacity: 1; - } - } -} - -.dscSidebarItem--active { - background: shade($euiColorLightestShade, 5%); - color: $euiColorFullShade; - font-weight: bold; -} - -/** - * 1. Truncate long text so it doesn't push the actions outside of the container. - */ -.dscSidebarItem__label { - overflow: hidden; /* 1 */ - text-overflow: ellipsis; /* 1 */ -} - -/** - * 1. Only visually hide the action, so that it's still accessible to screen readers. - * 2. When tabbed to, this element needs to be visible for keyboard accessibility. - */ -.dscSidebarItem__action { - opacity: 0; /* 1 */ - - &:focus { - opacity: 1; /* 2 */ - } -} - -.dscFieldSearch { - padding: $euiSizeS; -} - -.dscFieldFilter { - margin-top: $euiSizeS; -} - -.dscFieldDetails { - padding: $euiSizeS; - background-color: $euiColorLightestShade; - color: $euiTextColor; -} - -// SASSTODO: replace the padding and margin values with variables -.dscFieldDetails__progress { - background-color: $euiColorEmptyShade; - color: $euiColorDarkShade; - padding: $euiSizeXS; -} - -// SASSTODO: replace the margin-top value with a variable -.dscFieldDetailsItem { - margin-top: 5px; -} - -.dscFieldDetails__filter { - cursor: pointer; -} - -.dscFieldDetailsItem__title { - line-height: 1.5; - display: flex; - align-items: center; - justify-content: space-between; -} - -/** - * 1. If the field name is very long, don't let it squash the buttons. - */ -.dscFieldDetailsItem__buttonGroup { - flex: 0 0 auto; /* 1 */ -} - -.dscFieldDetailsItem__button { - appearance: none; - border: none; - padding: 0; - background-color: transparent; -} - -.dscFieldName--noResults { - color: $euiColorDarkShade; -} - .dscResults { h3 { margin: -20px 0 10px 0; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/css_truncate.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/css_truncate.ts deleted file mode 100644 index 6aa645ea9368e..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/css_truncate.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -export function createCssTruncateDirective() { - return { - restrict: 'A', - scope: {}, - link: ($scope: any, $elem: any, attrs: any) => { - $elem.css({ - overflow: 'hidden', - 'white-space': 'nowrap', - 'text-overflow': 'ellipsis', - 'word-break': 'break-all', - }); - - if (attrs.cssTruncateExpandable != null) { - $scope.$watch( - function() { - return $elem.html(); - }, - function() { - if ($elem[0].offsetWidth < $elem[0].scrollWidth) { - $elem.css({ cursor: 'pointer' }); - $elem.bind('click', function() { - $scope.toggle(); - }); - } - } - ); - } - - $scope.toggle = function() { - if ($elem.css('white-space') !== 'normal') { - $elem.css({ 'white-space': 'normal' }); - } else { - $elem.css({ 'white-space': 'nowrap' }); - } - }; - - $scope.$on('$destroy', function() { - $elem.unbind('click'); - $elem.unbind('mouseenter'); - }); - }, - }; -} diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html index e9338b8bd57cc..fb38f3e7d4c49 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html @@ -20,20 +20,21 @@

{{screenTitle}}

-