diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 43e32fd611074..d5ab85c54a728 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -911,7 +911,7 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); - test('MARK_VERSION_INDEX_READY -> MARK_VERSION_INDEX_CONFLICT if someone else removed the current alias from the source index', () => { + test('MARK_VERSION_INDEX_READY -> MARK_VERSION_INDEX_CONFLICT if another removed the current alias from the source index', () => { const res: ResponseType<'MARK_VERSION_INDEX_READY'> = Either.left({ type: 'alias_not_found_exception', }); @@ -920,6 +920,16 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('MARK_VERSION_INDEX_READY -> MARK_VERSION_INDEX_CONFLICT if another node removed the temporary index', () => { + const res: ResponseType<'MARK_VERSION_INDEX_READY'> = Either.left({ + type: 'index_not_found_exception', + index: '.kibana_7.11.0_reindex_temp', + }); + const newState = model(markVersionIndexReadyState, res); + expect(newState.controlState).toEqual('MARK_VERSION_INDEX_READY_CONFLICT'); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + }); }); describe('MARK_VERSION_INDEX_READY_CONFLICT', () => { const aliasActions = Option.some([Symbol('alias action')] as unknown) as Option.Some< diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index ba2508bf73cce..1119edde8e268 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -638,12 +638,20 @@ export const model = (currentState: State, resW: ResponseType): // alias_not_found_exception another instance has completed a // migration from the same source. return { ...stateP, controlState: 'MARK_VERSION_INDEX_READY_CONFLICT' }; - } else if ( - left.type === 'remove_index_not_a_concrete_index' || - left.type === 'index_not_found_exception' - ) { - // We don't handle these errors as the migration algorithm will never - // cause them to occur (these are only relevant to the LEGACY_DELETE + } else if (left.type === 'index_not_found_exception') { + if (left.index === stateP.tempIndex) { + // another instance has already completed the migration and deleted + // the temporary index + return { ...stateP, controlState: 'MARK_VERSION_INDEX_READY_CONFLICT' }; + } else { + // The migration algorithm will never cause a + // index_not_found_exception for an index other than the temporary + // index handled above. + throwBadResponse(stateP, left as never); + } + } else if (left.type === 'remove_index_not_a_concrete_index') { + // We don't handle this error as the migration algorithm will never + // cause it to occur (this error is only relevant to the LEGACY_DELETE // step). throwBadResponse(stateP, left as never); } else { diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_type_vega/public/data_model/search_api.ts index 4cb2a6119a2be..2d47102705139 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.ts +++ b/src/plugins/vis_type_vega/public/data_model/search_api.ts @@ -45,7 +45,10 @@ export class SearchAPI { }); if (this.inspectorAdapters) { - requestResponders[requestId] = this.inspectorAdapters.requests.start(requestId, request); + requestResponders[requestId] = this.inspectorAdapters.requests.start(requestId, { + ...request, + searchSessionId: this.searchSessionId, + }); requestResponders[requestId].json(params.body); } diff --git a/test/scripts/run_multiple_kibana_nodes.sh b/test/scripts/run_multiple_kibana_nodes.sh new file mode 100755 index 0000000000000..f5661c19bed11 --- /dev/null +++ b/test/scripts/run_multiple_kibana_nodes.sh @@ -0,0 +1,86 @@ +# 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. + +#!/bin/bash + +# +# Script to run multiple kibana nodes in parallel on the same machine. +# Make sure to run the script from kibana root directory. Some functions depend on the jq command-line utility +# being installed. +# +# bash test/scripts/run_multiple_kibana_nodes.sh [options] +# functions: +# start [instances] [args] - start multiple kibanas (3 default) +# es [args] - run elasticsearch +# tail - show logs of all kibanas +# kill - kills all started kibana processes +# clean - clean up nohup files +# kibana_index - search .kibana index against es +# + +FN="$1" + +if [ "${FN}" == "kill" ]; then + echo "killing main processes" + for pid in $(cat processes.out); do kill -9 $pid; done + echo "killing trailing processes" + for pid in $(pgrep -f scripts/kibana); do kill -9 $pid; done + exit 0; +fi + +if [ "${FN}" == "tail" ]; then + tail -f nohup_* + exit 0; +fi + +if [ "${FN}" == "clean" ]; then + rm -r nohup_*.out + rm processes.out + exit 0; +fi + +if [ "${FN}" == "es" ]; then + ARGS="$2" + yarn es snapshot $ARGS + exit 0; +fi + +if [ "${FN}" == "kibana_index" ]; then + # search the kibana index + curl -XPOST http://elastic:changeme@localhost:9200/.kibana/_search -u elastic:changeme -d '' | jq + exit 0; +fi + +if [ "${FN}" == "start" ]; then + NUM="$2" + ARGS="$3" + if test ! "${NUM-}"; then + NUM=3 + fi + node scripts/build_kibana_platform_plugins --no-examples + rm processes.out + for i in $(seq 0 $(expr $NUM - 1)) + do + PORT="56${i}1" + PROXY="56${i}3" + echo "starting kibana on port $PORT" + nohup node scripts/kibana.js --dev.basePathProxyTarget=$PROXY --server.port=$PORT --dev --no-watch --no-optimizer --no-base-path $ARGS > nohup_$i.out & + PROCESS_ID=$! + echo "${PROCESS_ID}" >> processes.out + done + exit 0; +fi diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index c810bd3e7c489..447f6d502fe40 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -18,7 +18,7 @@ import { UXMetrics } from './UXMetrics'; import { ImpactfulMetrics } from './ImpactfulMetrics'; import { PageLoadAndViews } from './Panels/PageLoadAndViews'; import { VisitorBreakdownsPanel } from './Panels/VisitorBreakdowns'; -import { useBreakPoints } from './hooks/useBreakPoints'; +import { useBreakPoints } from '../../../hooks/use_break_points'; import { getPercentileLabel } from './UXMetrics/translations'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index 6810b56fb8f87..b1a97dd34b887 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -113,7 +113,7 @@ export function TransactionDetails({

{transactionName}

- + diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 8776b7e9355f1..2f83d48049486 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -129,7 +129,7 @@ export function ServiceInventory() { return ( <> - + diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index e501dd3bb7a56..eb8068bc8114d 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -57,6 +57,8 @@ function wrapper({ children }: { children?: ReactNode }) { rangeTo: 'now', start: 'mystart', end: 'myend', + comparisonEnabled: true, + comparisonType: 'yesterday', }} > {children} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index c6cc59876fe35..bf03b60d6d24f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -12,6 +12,7 @@ import { isRumAgentName } from '../../../../common/agent_name'; import { AnnotationsContextProvider } from '../../../context/annotations/annotations_context'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; +import { useBreakPoints } from '../../../hooks/use_break_points'; import { LatencyChart } from '../../shared/charts/latency_chart'; import { TransactionBreakdownChart } from '../../shared/charts/transaction_breakdown_chart'; import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; @@ -22,7 +23,6 @@ import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; import { ServiceOverviewInstancesChartAndTable } from './service_overview_instances_chart_and_table'; import { ServiceOverviewThroughputChart } from './service_overview_throughput_chart'; import { ServiceOverviewTransactionsTable } from './service_overview_transactions_table'; -import { useShouldUseMobileLayout } from './use_should_use_mobile_layout'; /** * The height a chart should be if it's next to a table with 5 rows and a title. @@ -44,8 +44,8 @@ export function ServiceOverview({ // The default EuiFlexGroup breaks at 768, but we want to break at 992, so we // observe the window width and set the flex directions of rows accordingly - const shouldUseMobileLayout = useShouldUseMobileLayout(); - const rowDirection = shouldUseMobileLayout ? 'column' : 'row'; + const { isMedium } = useBreakPoints(); + const rowDirection = isMedium ? 'column' : 'row'; const { transactionType } = useApmServiceContext(); const transactionTypeLabel = i18n.translate( @@ -57,7 +57,7 @@ export function ServiceOverview({ return ( - + {isRumAgent && ( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx index 76db81a70550d..bbd6dd1d498d3 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx @@ -6,7 +6,7 @@ import React, { ReactNode } from 'react'; import styled from 'styled-components'; -import { useShouldUseMobileLayout } from './use_should_use_mobile_layout'; +import { useBreakPoints } from '../../../hooks/use_break_points'; /** * The height for a table on the overview page. Is the height of a 5-row basic @@ -58,12 +58,12 @@ export function ServiceOverviewTableContainer({ children?: ReactNode; isEmptyAndLoading: boolean; }) { - const shouldUseMobileLayout = useShouldUseMobileLayout(); + const { isMedium } = useBreakPoints(); return ( {children} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/use_should_use_mobile_layout.ts b/x-pack/plugins/apm/public/components/app/service_overview/use_should_use_mobile_layout.ts deleted file mode 100644 index bd844a3f2e694..0000000000000 --- a/x-pack/plugins/apm/public/components/app/service_overview/use_should_use_mobile_layout.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isWithinMaxBreakpoint } from '@elastic/eui'; -import { useEffect, useState } from 'react'; - -export function useShouldUseMobileLayout() { - const [shouldUseMobileLayout, setShouldUseMobileLayout] = useState( - isWithinMaxBreakpoint(window.innerWidth, 'm') - ); - - useEffect(() => { - const resizeHandler = () => { - setShouldUseMobileLayout(isWithinMaxBreakpoint(window.innerWidth, 'm')); - }; - window.addEventListener('resize', resizeHandler); - - return () => { - window.removeEventListener('resize', resizeHandler); - }; - }); - - return shouldUseMobileLayout; -} diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 30fbfe9cc8708..5be08477b3bf5 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -108,7 +108,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { return ( <> - + diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index bfb9c51bc245e..31d90330721da 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -130,7 +130,7 @@ describe('TransactionOverview', () => { }); expect(history.location.search).toEqual( - '?transactionType=secondType&rangeFrom=now-15m&rangeTo=now' + '?transactionType=secondType&rangeFrom=now-15m&rangeTo=now&comparisonEnabled=true&comparisonType=yesterday' ); expect(getByText(container, 'firstType')).toBeInTheDocument(); expect(getByText(container, 'secondType')).toBeInTheDocument(); @@ -139,7 +139,7 @@ describe('TransactionOverview', () => { expect(history.push).toHaveBeenCalled(); expect(history.location.search).toEqual( - '?transactionType=firstType&rangeFrom=now-15m&rangeTo=now' + '?transactionType=firstType&rangeFrom=now-15m&rangeTo=now&comparisonEnabled=true&comparisonType=yesterday' ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts index 8576d9ee86353..e899e4a07e96f 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -85,6 +85,8 @@ export type APMQueryParams = { searchTerm?: string; percentile?: 50 | 75 | 90 | 95 | 99; latencyAggregationType?: string; + comparisonEnabled?: boolean; + comparisonType?: string; } & { [key in LocalUIFilterName]?: string }; // forces every value of T[K] to be type: string diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 6382f4937ac0e..48d329a853327 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -7,22 +7,49 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; +import { px, unit } from '../../style/variables'; import { DatePicker } from './DatePicker'; import { KueryBar } from './KueryBar'; +import { TimeComparison } from './time_comparison'; +import { useBreakPoints } from '../../hooks/use_break_points'; const SearchBarFlexGroup = styled(EuiFlexGroup)` margin: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeM} -${theme.eui.gutterTypes.gutterMedium} ${theme.eui.euiSizeM}`}; `; -export function SearchBar(props: { prepend?: React.ReactNode | string }) { +interface Props { + prepend?: React.ReactNode | string; + showTimeComparison?: boolean; +} + +function getRowDirection(showColumn: boolean) { + return showColumn ? 'column' : 'row'; +} + +export function SearchBar({ prepend, showTimeComparison = false }: Props) { + const { isMedium, isLarge } = useBreakPoints(); + const itemsStyle = { marginBottom: isLarge ? px(unit) : 0 }; return ( - - - + + + - - + + + {showTimeComparison && ( + + + + )} + + + + ); diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx new file mode 100644 index 0000000000000..6348097a3e3ad --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { EuiThemeProvider } from '../../../../../observability/public'; +import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; +import { IUrlParams } from '../../../context/url_params_context/types'; +import { + expectTextsInDocument, + expectTextsNotInDocument, +} from '../../../utils/testHelpers'; +import { TimeComparison } from './'; +import * as urlHelpers from '../../shared/Links/url_helpers'; + +function getWrapper(params?: IUrlParams) { + return ({ children }: { children?: ReactNode }) => { + return ( + + + {children} + + + ); + }; +} + +describe('TimeComparison', () => { + const spy = jest.spyOn(urlHelpers, 'replace'); + beforeEach(() => { + jest.resetAllMocks(); + }); + describe('Time range is between 0 - 24 hours', () => { + it('sets default values', () => { + const Wrapper = getWrapper({ + start: '2021-01-28T14:45:00.000Z', + end: '2021-01-28T15:00:00.000Z', + }); + render(, { + wrapper: Wrapper, + }); + expect(spy).toHaveBeenCalledWith(expect.anything(), { + query: { + comparisonEnabled: 'true', + comparisonType: 'yesterday', + }, + }); + }); + it('selects yesterday and enables comparison', () => { + const Wrapper = getWrapper({ + start: '2021-01-28T14:45:00.000Z', + end: '2021-01-28T15:00:00.000Z', + comparisonEnabled: true, + comparisonType: 'yesterday', + }); + const component = render(, { + wrapper: Wrapper, + }); + expectTextsInDocument(component, ['Yesterday', 'A week ago']); + expect( + (component.getByTestId('comparisonSelect') as HTMLSelectElement) + .selectedIndex + ).toEqual(0); + }); + }); + + describe('Time range is between 24 hours - 1 week', () => { + it('sets default values', () => { + const Wrapper = getWrapper({ + start: '2021-01-26T15:00:00.000Z', + end: '2021-01-28T15:00:00.000Z', + }); + render(, { + wrapper: Wrapper, + }); + expect(spy).toHaveBeenCalledWith(expect.anything(), { + query: { + comparisonEnabled: 'true', + comparisonType: 'week', + }, + }); + }); + it('selects week and enables comparison', () => { + const Wrapper = getWrapper({ + start: '2021-01-26T15:00:00.000Z', + end: '2021-01-28T15:00:00.000Z', + comparisonEnabled: true, + comparisonType: 'week', + }); + const component = render(, { + wrapper: Wrapper, + }); + expectTextsNotInDocument(component, ['Yesterday']); + expectTextsInDocument(component, ['A week ago']); + expect( + (component.getByTestId('comparisonSelect') as HTMLSelectElement) + .selectedIndex + ).toEqual(0); + }); + }); + + describe('Time range is greater than 7 days', () => { + it('Shows absolute times without year when within the same year', () => { + const Wrapper = getWrapper({ + start: '2021-01-20T15:00:00.000Z', + end: '2021-01-28T15:00:00.000Z', + comparisonEnabled: true, + comparisonType: 'previousPeriod', + }); + const component = render(, { + wrapper: Wrapper, + }); + expect(spy).not.toHaveBeenCalled(); + expectTextsInDocument(component, ['20/01 - 28/01']); + expect( + (component.getByTestId('comparisonSelect') as HTMLSelectElement) + .selectedIndex + ).toEqual(0); + }); + + it('Shows absolute times with year when on different year', () => { + const Wrapper = getWrapper({ + start: '2020-12-20T15:00:00.000Z', + end: '2021-01-28T15:00:00.000Z', + comparisonEnabled: true, + comparisonType: 'previousPeriod', + }); + const component = render(, { + wrapper: Wrapper, + }); + expect(spy).not.toHaveBeenCalled(); + expectTextsInDocument(component, ['20/12/20 - 28/01/21']); + expect( + (component.getByTestId('comparisonSelect') as HTMLSelectElement) + .selectedIndex + ).toEqual(0); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx new file mode 100644 index 0000000000000..2195621167e83 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -0,0 +1,144 @@ +/* + * 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 { EuiCheckbox, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import styled from 'styled-components'; +import { getDateDifference } from '../../../../common/utils/formatters'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { px, unit } from '../../../style/variables'; +import * as urlHelpers from '../../shared/Links/url_helpers'; +import { useBreakPoints } from '../../../hooks/use_break_points'; + +const PrependContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + background-color: ${({ theme }) => theme.eui.euiGradientMiddle}; + padding: 0 ${px(unit)}; +`; + +function formatPreviousPeriodDates({ + momentStart, + momentEnd, +}: { + momentStart: moment.Moment; + momentEnd: moment.Moment; +}) { + const isDifferentYears = momentStart.get('year') !== momentEnd.get('year'); + const dateFormat = isDifferentYears ? 'DD/MM/YY' : 'DD/MM'; + return `${momentStart.format(dateFormat)} - ${momentEnd.format(dateFormat)}`; +} + +function getSelectOptions({ start, end }: { start?: string; end?: string }) { + const momentStart = moment(start); + const momentEnd = moment(end); + const dateDiff = getDateDifference(momentStart, momentEnd, 'days'); + + const yesterdayOption = { + value: 'yesterday', + text: i18n.translate('xpack.apm.timeComparison.select.yesterday', { + defaultMessage: 'Yesterday', + }), + }; + + const aWeekAgoOption = { + value: 'week', + text: i18n.translate('xpack.apm.timeComparison.select.weekAgo', { + defaultMessage: 'A week ago', + }), + }; + + const prevPeriodOption = { + value: 'previousPeriod', + text: formatPreviousPeriodDates({ momentStart, momentEnd }), + }; + + // Less than one day + if (dateDiff < 1) { + return [yesterdayOption, aWeekAgoOption]; + } + + // Less than one week + if (dateDiff <= 7) { + return [aWeekAgoOption]; + } + + // above one week + return [prevPeriodOption]; +} + +export function TimeComparison() { + const history = useHistory(); + const { isMedium, isLarge } = useBreakPoints(); + const { + urlParams: { start, end, comparisonEnabled, comparisonType }, + } = useUrlParams(); + + const selectOptions = getSelectOptions({ start, end }); + + // Sets default values + if (comparisonEnabled === undefined || comparisonType === undefined) { + urlHelpers.replace(history, { + query: { + comparisonEnabled: comparisonEnabled === false ? 'false' : 'true', + comparisonType: comparisonType + ? comparisonType + : selectOptions[0].value, + }, + }); + return null; + } + + const isSelectedComparisonTypeAvailable = selectOptions.some( + ({ value }) => value === comparisonType + ); + + // Replaces type when current one is no longer available in the select options + if (selectOptions.length !== 0 && !isSelectedComparisonTypeAvailable) { + urlHelpers.replace(history, { + query: { comparisonType: selectOptions[0].value }, + }); + return null; + } + + return ( + + 0} + onChange={() => { + urlHelpers.push(history, { + query: { + comparisonEnabled: Boolean(!comparisonEnabled).toString(), + }, + }); + }} + /> + + } + onChange={(e) => { + urlHelpers.push(history, { + query: { + comparisonType: e.target.value, + }, + }); + }} + /> + ); +} diff --git a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts index 0596d649116a0..1b8a131bd88a3 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts @@ -49,6 +49,8 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { searchTerm, percentile, latencyAggregationType = LatencyAggregationType.avg, + comparisonEnabled, + comparisonType, } = query; const localUIFilters = pickKeys(query, ...localUIFilterNames); @@ -78,6 +80,10 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { searchTerm: toString(searchTerm), percentile: toNumber(percentile), latencyAggregationType, + comparisonEnabled: comparisonEnabled + ? toBoolean(comparisonEnabled) + : undefined, + comparisonType, // ui filters environment, diff --git a/x-pack/plugins/apm/public/context/url_params_context/types.ts b/x-pack/plugins/apm/public/context/url_params_context/types.ts index d792c93b7d0dc..cd5fa55cd132f 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/types.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/types.ts @@ -29,4 +29,6 @@ export type IUrlParams = { searchTerm?: string; percentile?: number; latencyAggregationType?: string; + comparisonEnabled?: boolean; + comparisonType?: string; } & Partial>; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useBreakPoints.ts b/x-pack/plugins/apm/public/hooks/use_break_points.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useBreakPoints.ts rename to x-pack/plugins/apm/public/hooks/use_break_points.ts diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json b/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json index 5a810fa90259a..4a791ed18121e 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json @@ -4,7 +4,11 @@ "./plugins/observability/**/*", "./typings/**/*" ], - "exclude": ["**/__fixtures__/**/*", "./plugins/apm/e2e/cypress/**/*"], + "exclude": [ + "**/__fixtures__/**/*", + "./plugins/apm/e2e", + "./plugins/apm/ftr_e2e" + ], "compilerOptions": { "noErrorTruncation": true } diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 5e38cb49114e9..5aec6b02c057c 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -10,7 +10,7 @@ import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { App } from './app'; import { LensAppProps, LensAppServices } from './types'; -import { EditorFrameInstance } from '../types'; +import { EditorFrameInstance, EditorFrameProps } from '../types'; import { Document } from '../persistence'; import { DOC_TYPE } from '../../common'; import { mount } from 'enzyme'; @@ -44,6 +44,8 @@ import { import { LensAttributeService } from '../lens_attribute_service'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { EmbeddableStateTransfer } from '../../../../../src/plugins/embeddable/public'; +import { NativeRenderer } from '../native_renderer'; +import moment from 'moment'; jest.mock('../editor_frame_service/editor_frame/expression_helpers'); jest.mock('src/core/public'); @@ -144,6 +146,11 @@ function createMockTimefilter() { return unsubscribe; }, }), + calculateBounds: jest.fn(() => ({ + min: moment('2021-01-10T04:00:00.000Z'), + max: moment('2021-01-10T08:00:00.000Z'), + })), + getBounds: jest.fn(() => timeFilter), getRefreshInterval: () => {}, getRefreshIntervalDefaults: () => {}, getAutoRefreshFetch$: () => ({ @@ -233,6 +240,9 @@ describe('Lens App', () => { }), }, search: createMockSearchService(), + nowProvider: { + get: jest.fn(), + }, } as unknown) as DataPublicPluginStart, storage: { get: jest.fn(), @@ -306,8 +316,8 @@ describe('Lens App', () => { />, Object { "dateRange": Object { - "fromDate": "now-7d", - "toDate": "now", + "fromDate": "2021-01-10T04:00:00.000Z", + "toDate": "2021-01-10T08:00:00.000Z", }, "doc": undefined, "filters": Array [], @@ -350,7 +360,7 @@ describe('Lens App', () => { expect(frame.mount).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ - dateRange: { fromDate: 'now-7d', toDate: 'now' }, + dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, query: { query: '', language: 'kuery' }, filters: [pinnedFilter], }) @@ -1008,7 +1018,7 @@ describe('Lens App', () => { expect(frame.mount).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ - dateRange: { fromDate: 'now-7d', toDate: 'now' }, + dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, query: { query: '', language: 'kuery' }, }) ); @@ -1055,7 +1065,11 @@ describe('Lens App', () => { }); it('updates the editor frame when the user changes query or time in the search bar', () => { - const { component, frame } = mountWith({}); + const { component, frame, services } = mountWith({}); + (services.data.query.timefilter.timefilter.calculateBounds as jest.Mock).mockReturnValue({ + min: moment('2021-01-09T04:00:00.000Z'), + max: moment('2021-01-09T08:00:00.000Z'), + }); act(() => component.find(TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, @@ -1071,10 +1085,14 @@ describe('Lens App', () => { }), {} ); + expect(services.data.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({ + from: 'now-14d', + to: 'now-7d', + }); expect(frame.mount).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ - dateRange: { fromDate: 'now-14d', toDate: 'now-7d' }, + dateRange: { fromDate: '2021-01-09T04:00:00.000Z', toDate: '2021-01-09T08:00:00.000Z' }, query: { query: 'new', language: 'lucene' }, }) ); @@ -1237,6 +1255,34 @@ describe('Lens App', () => { ); }); + it('clears all existing unpinned filters when the active saved query is cleared', () => { + const { component, frame, services } = mountWith({}); + act(() => + component.find(TopNavMenu).prop('onQuerySubmit')!({ + dateRange: { from: 'now-14d', to: 'now-7d' }, + query: { query: 'new', language: 'lucene' }, + }) + ); + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const field = ({ name: 'myfield' } as unknown) as IFieldType; + const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + const unpinned = esFilters.buildExistsFilter(field, indexPattern); + const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); + FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); + act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); + component.update(); + act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); + component.update(); + expect(frame.mount).toHaveBeenLastCalledWith( + expect.any(Element), + expect.objectContaining({ + filters: [pinned], + }) + ); + }); + }); + + describe('search session id management', () => { it('updates the searchSessionId when the query is updated', () => { const { component, frame } = mountWith({}); act(() => { @@ -1263,12 +1309,12 @@ describe('Lens App', () => { expect(frame.mount).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ - searchSessionId: `sessionId-1`, + searchSessionId: `sessionId-2`, }) ); }); - it('clears all existing unpinned filters when the active saved query is cleared', () => { + it('updates the searchSessionId when the active saved query is cleared', () => { const { component, frame, services } = mountWith({}); act(() => component.find(TopNavMenu).prop('onQuerySubmit')!({ @@ -1286,31 +1332,67 @@ describe('Lens App', () => { component.update(); act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); component.update(); - expect(frame.mount).toHaveBeenLastCalledWith( + expect(frame.mount).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ - filters: [pinned], + searchSessionId: `sessionId-2`, }) ); }); - it('updates the searchSessionId when the active saved query is cleared', () => { - const { component, frame, services } = mountWith({}); - act(() => - component.find(TopNavMenu).prop('onQuerySubmit')!({ - dateRange: { from: 'now-14d', to: 'now-7d' }, - query: { query: 'new', language: 'lucene' }, + const mockUpdate = { + filterableIndexPatterns: [], + doc: { + title: '', + description: '', + visualizationType: '', + state: { + datasourceStates: {}, + visualization: {}, + filters: [], + query: { query: '', language: 'lucene' }, + }, + references: [], + }, + isSaveable: true, + activeData: undefined, + }; + + it('does not update the searchSessionId when the state changes', () => { + const { component, frame } = mountWith({}); + act(() => { + (component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange( + mockUpdate + ); + }); + component.update(); + expect(frame.mount).not.toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + searchSessionId: `sessionId-2`, }) ); - const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; - const field = ({ name: 'myfield' } as unknown) as IFieldType; - const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; - const unpinned = esFilters.buildExistsFilter(field, indexPattern); - const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); - FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); - act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); - component.update(); - act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); + }); + + it('does update the searchSessionId when the state changes and too much time passed', () => { + const { component, frame, services } = mountWith({}); + + // time range is 100,000ms ago to 30,000ms ago (that's a lag of 30 percent) + (services.data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000)); + (services.data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ + from: 'now-2m', + to: 'now', + }); + (services.data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({ + min: moment(Date.now() - 100000), + max: moment(Date.now() - 30000), + }); + + act(() => { + (component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange( + mockUpdate + ); + }); component.update(); expect(frame.mount).toHaveBeenCalledWith( expect.any(Element), @@ -1319,6 +1401,34 @@ describe('Lens App', () => { }) ); }); + + it('does not update the searchSessionId when the state changes and too little time has passed', () => { + const { component, frame, services } = mountWith({}); + + // time range is 100,000ms ago to 300ms ago (that's a lag of .3 percent, not enough to trigger a session update) + (services.data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 300)); + (services.data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ + from: 'now-2m', + to: 'now', + }); + (services.data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({ + min: moment(Date.now() - 100000), + max: moment(Date.now() - 300), + }); + + act(() => { + (component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange( + mockUpdate + ); + }); + component.update(); + expect(frame.mount).not.toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + searchSessionId: `sessionId-2`, + }) + ); + }); }); describe('showing a confirm message when leaving', () => { diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 3f10cb341105c..28e1f6da60742 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -7,7 +7,7 @@ import './app.scss'; import _ from 'lodash'; -import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; import { EuiBreadcrumb } from '@elastic/eui'; @@ -39,6 +39,7 @@ import { LensByReferenceInput, LensEmbeddableInput, } from '../editor_frame_service/embeddable/embeddable'; +import { useTimeRange } from './time_range'; export function App({ history, @@ -107,9 +108,11 @@ export function App({ state.searchSessionId, ]); - // Need a stable reference for the frame component of the dateRange - const { from: fromDate, to: toDate } = data.query.timefilter.timefilter.getTime(); - const currentDateRange = useMemo(() => ({ fromDate, toDate }), [fromDate, toDate]); + const { resolvedDateRange, from: fromDate, to: toDate } = useTimeRange( + data, + state.lastKnownDoc, + setState + ); const onError = useCallback( (e: { message: string }) => @@ -658,7 +661,7 @@ export function App({ render={editorFrame.mount} nativeProps={{ searchSessionId: state.searchSessionId, - dateRange: currentDateRange, + dateRange: resolvedDateRange, query: state.query, filters: state.filters, savedQuery: state.savedQuery, @@ -670,7 +673,7 @@ export function App({ if (isSaveable !== state.isSaveable) { setState((s) => ({ ...s, isSaveable })); } - if (!_.isEqual(state.persistedDoc, doc)) { + if (!_.isEqual(state.persistedDoc, doc) && !_.isEqual(state.lastKnownDoc, doc)) { setState((s) => ({ ...s, lastKnownDoc: doc })); } if (!_.isEqual(state.activeData, activeData)) { diff --git a/x-pack/plugins/lens/public/app_plugin/time_range.ts b/x-pack/plugins/lens/public/app_plugin/time_range.ts new file mode 100644 index 0000000000000..5a9f2d5ab9e90 --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/time_range.ts @@ -0,0 +1,82 @@ +/* + * 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 './app.scss'; + +import _ from 'lodash'; +import moment from 'moment'; +import { useEffect, useMemo } from 'react'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { LensAppState } from './types'; +import { Document } from '../persistence'; + +function containsDynamicMath(dateMathString: string) { + return dateMathString.includes('now'); +} + +const TIME_LAG_PERCENTAGE_LIMIT = 0.02; + +/** + * Fetches the current global time range from data plugin and restarts session + * if the fixed "now" parameter is diverging too much from the actual current time. + * @param data data plugin contract to manage current now value, time range and session + * @param lastKnownDoc Current state of the editor + * @param setState state setter for Lens app state + */ +export function useTimeRange( + data: DataPublicPluginStart, + lastKnownDoc: Document | undefined, + setState: React.Dispatch> +) { + const timefilter = data.query.timefilter.timefilter; + const { from, to } = data.query.timefilter.timefilter.getTime(); + const currentNow = data.nowProvider.get(); + + // Need a stable reference for the frame component of the dateRange + const resolvedDateRange = useMemo(() => { + const { min, max } = timefilter.calculateBounds({ + from, + to, + }); + return { fromDate: min?.toISOString() || from, toDate: max?.toISOString() || to }; + // recalculate current date range if current "now" value changes because calculateBounds + // depends on it internally + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [timefilter, currentNow, from, to]); + + useEffect(() => { + const unresolvedTimeRange = timefilter.getTime(); + if ( + !containsDynamicMath(unresolvedTimeRange.from) && + !containsDynamicMath(unresolvedTimeRange.to) + ) { + return; + } + + const { min, max } = timefilter.getBounds(); + + if (!min || !max) { + // bounds not fully specified, bailing out + return; + } + + // calculate length of currently configured range in ms + const timeRangeLength = moment.duration(max.diff(min)).asMilliseconds(); + + // calculate lag of managed "now" for date math + const nowDiff = Date.now() - data.nowProvider.get().valueOf(); + + // if the lag is signifcant, start a new session to clear the cache + if (nowDiff > timeRangeLength * TIME_LAG_PERCENTAGE_LIMIT) { + setState((s) => ({ + ...s, + searchSessionId: data.search.session.start(), + })); + } + }, [data.nowProvider, data.search.session, timefilter, lastKnownDoc, setState]); + + return { resolvedDateRange, from, to }; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 2cb815596d8b9..f908d16afe470 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -252,7 +252,6 @@ export function EditorFrame(props: EditorFrameProps) { state.visualization, state.activeData, props.query, - props.dateRange, props.filters, props.savedQuery, state.title, diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx index 5b175eb06a4a3..edf038160e0a8 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useEffect, useCallback, useState } from 'react'; +import React, { FC, useEffect, useCallback, useState, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { @@ -50,15 +50,24 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { const [closeJobModalVisible, setCloseJobModalVisible] = useState(null); const [combinedJobState, setCombinedJobState] = useState(null); + const isMounted = useRef(true); useEffect(() => { loadModelSnapshots(); + return () => { + isMounted.current = false; + }; }, []); - const loadModelSnapshots = useCallback(async () => { + async function loadModelSnapshots() { + if (isMounted.current === false) { + // table refresh can be triggered a while after a snapshot revert has been triggered. + // ensure the table is still visible before attempted to refresh it. + return; + } const { model_snapshots: ms } = await ml.getModelSnapshots(job.job_id); setSnapshots(ms); setSnapshotsLoaded(true); - }, [job]); + } const checkJobIsClosed = useCallback( async (snapshot: ModelSnapshot) => { @@ -107,13 +116,14 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { } }, []); - const closeRevertFlyout = useCallback((reload: boolean) => { + const closeRevertFlyout = useCallback(() => { setRevertSnapshot(null); - if (reload) { - loadModelSnapshots(); - // wait half a second before refreshing the jobs list - setTimeout(refreshJobList, 500); - } + }, []); + + const refresh = useCallback(() => { + loadModelSnapshots(); + // wait half a second before refreshing the jobs list + setTimeout(refreshJobList, 500); }, []); const columns: Array> = [ @@ -231,6 +241,7 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { snapshots={snapshots} job={job} closeFlyout={closeRevertFlyout} + refresh={refresh} /> )} diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index 62f5623f67964..05e624c194e6e 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -53,10 +53,17 @@ interface Props { snapshot: ModelSnapshot; snapshots: ModelSnapshot[]; job: CombinedJobWithStats; - closeFlyout(reload: boolean): void; + closeFlyout(): void; + refresh(): void; } -export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, closeFlyout }) => { +export const RevertModelSnapshotFlyout: FC = ({ + snapshot, + snapshots, + job, + closeFlyout, + refresh, +}) => { const { toasts } = useNotifications(); const { loadAnomalyDataForJob, loadEventRateForJob } = useMemo( () => chartLoaderProvider(mlResultsService), @@ -73,7 +80,6 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, const [eventRateData, setEventRateData] = useState([]); const [anomalies, setAnomalies] = useState([]); const [chartReady, setChartReady] = useState(false); - const [applying, setApplying] = useState(false); useEffect(() => { createChartData(); @@ -110,13 +116,6 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, setChartReady(true); }, [job]); - function closeWithReload() { - closeFlyout(true); - } - function closeWithoutReload() { - closeFlyout(false); - } - function showRevertModal() { setRevertModalVisible(true); } @@ -125,7 +124,6 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, } async function applyRevert() { - setApplying(true); const end = replay && runInRealTime === false ? job.data_counts.latest_record_timestamp : undefined; try { @@ -138,17 +136,19 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, })) : undefined; - await ml.jobs.revertModelSnapshot( - job.job_id, - currentSnapshot.snapshot_id, - replay, - end, - events - ); + ml.jobs + .revertModelSnapshot(job.job_id, currentSnapshot.snapshot_id, replay, end, events) + .then(() => { + toasts.addSuccess( + i18n.translate('xpack.ml.revertModelSnapshotFlyout.revertSuccessTitle', { + defaultMessage: 'Model snapshot revert successful', + }) + ); + refresh(); + }); hideRevertModal(); - closeWithReload(); + closeFlyout(); } catch (error) { - setApplying(false); toasts.addError(new Error(error.body.message), { title: i18n.translate('xpack.ml.revertModelSnapshotFlyout.revertErrorTitle', { defaultMessage: 'Model snapshot revert failed', @@ -166,7 +166,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, return ( <> - +
@@ -347,7 +347,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, - + = ({ snapshot, snapshots, job, defaultMessage: 'Apply', } )} - confirmButtonDisabled={applying} buttonColor="danger" defaultFocusedButton="confirm" - /> + > + + )} diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index ab2270b4ce70f..72f325bfc2d6f 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -17,8 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const comboBox = getService('comboBox'); const supertest = getService('supertest'); - // FLAKY: https://github.com/elastic/kibana/issues/88796 - describe.skip('Connectors', function () { + describe('Connectors', function () { const objectRemover = new ObjectRemover(supertest); before(async () => { @@ -285,7 +284,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.setValue('nameInput', connectorName); - await comboBox.set('connectorIndexesComboBox', indexName); + await retry.try(async () => { + // At times we find the driver controlling the ComboBox in tests + // can select the wrong item, this ensures we always select the correct index + await comboBox.set('connectorIndexesComboBox', indexName); + expect( + await comboBox.isOptionSelected( + await testSubjects.find('connectorIndexesComboBox'), + indexName + ) + ).to.be(true); + }); await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); await pageObjects.common.closeToast();