diff --git a/packages/kbn-es-query/src/es_query/index.d.ts b/packages/kbn-es-query/src/es_query/index.d.ts new file mode 100644 index 0000000000000..9510a18441e53 --- /dev/null +++ b/packages/kbn-es-query/src/es_query/index.d.ts @@ -0,0 +1,39 @@ +/* + * 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 buildQueryFromFilters(filters: unknown[], indexPattern: unknown): unknown; +export function buildEsQuery( + indexPattern: unknown, + queries: unknown, + filters: unknown, + config?: { + allowLeadingWildcards: boolean; + queryStringOptions: unknown; + ignoreFilterIfFieldNotInIndex: boolean; + dateFormatTZ?: string | null; + } +): unknown; +export function getEsQueryConfig(config: { + get: (name: string) => unknown; +}): { + allowLeadingWildcards: boolean; + queryStringOptions: unknown; + ignoreFilterIfFieldNotInIndex: boolean; + dateFormatTZ?: string | null; +}; diff --git a/packages/kbn-es-query/src/index.d.ts b/packages/kbn-es-query/src/index.d.ts index 873636a28889f..ca4455da33f45 100644 --- a/packages/kbn-es-query/src/index.d.ts +++ b/packages/kbn-es-query/src/index.d.ts @@ -17,5 +17,6 @@ * under the License. */ +export * from './es_query'; export * from './kuery'; export * from './filters'; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index d96e3be8d7e38..b9554310e2413 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -18,7 +18,7 @@ */ // /// Define plugin function -import { DataPlugin as Plugin, DataSetup } from './plugin'; +import { DataPlugin as Plugin, DataSetup, DataStart } from './plugin'; export function plugin() { return new Plugin(); @@ -28,6 +28,7 @@ export function plugin() { /** @public types */ export type DataSetup = DataSetup; +export type DataStart = DataStart; export { FilterBar, ApplyFiltersPopover } from './filter'; export { diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index ed2a6638aba11..06ceace7e9e44 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -66,14 +66,18 @@ export interface SearchBarOwnProps { showFilterBar?: boolean; showDatePicker?: boolean; showAutoRefreshOnly?: boolean; - showSaveQuery?: boolean; - + onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; // Query bar - should be in SearchBarInjectedDeps query?: Query; + // Show when user has privileges to save + showSaveQuery?: boolean; savedQuery?: SavedQuery; onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }) => void; + // User has saved the current state as a saved query onSaved?: (savedQuery: SavedQuery) => void; + // User has modified the saved query, your app should persist the update onSavedQueryUpdated?: (savedQuery: SavedQuery) => void; + // User has cleared the active query, your app should clear the entire query bar onClearSavedQuery?: () => void; } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx index e4519a82f9556..ce39003da21f8 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx @@ -9,8 +9,6 @@ import { EuiTitle, EuiFlexGroup, EuiFlexItem, - EuiSpacer, - EuiBetaBadge, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -24,28 +22,9 @@ const TransactionBreakdownHeader: React.FC<{

- - - {i18n.translate('xpack.apm.transactionBreakdown.chartTitle', { - defaultMessage: 'Time spent by span type' - })} - - - - - - + {i18n.translate('xpack.apm.transactionBreakdown.chartTitle', { + defaultMessage: 'Time spent by span type' + })}

diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts index d8e8b586d2b82..03c36f2575f87 100644 --- a/x-pack/legacy/plugins/canvas/i18n/components.ts +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -214,6 +214,33 @@ export const ComponentStrings = { defaultMessage: 'Closes keyboard shortcuts reference', }), }, + PageManager: { + getPageNumberAriaLabel: (pageNumber: number) => + i18n.translate('xpack.canvas.pageManager.pageNumberAriaLabel', { + defaultMessage: 'Load page number {pageNumber}', + values: { + pageNumber, + }, + }), + }, + PagePreviewPageControls: { + getClonePageAriaLabel: () => + i18n.translate('xpack.canvas.pagePreviewPageControls.clonePageAriaLabel', { + defaultMessage: 'Clone page', + }), + getClonePageTooltip: () => + i18n.translate('xpack.canvas.pagePreviewPageControls.clonePageTooltip', { + defaultMessage: 'Clone', + }), + getDeletePageAriaLabel: () => + i18n.translate('xpack.canvas.pagePreviewPageControls.deletePageAriaLabel', { + defaultMessage: 'Delete page', + }), + getDeletePageTooltip: () => + i18n.translate('xpack.canvas.pagePreviewPageControls.deletePageTooltip', { + defaultMessage: 'Delete', + }), + }, Toolbar: { getEditorButtonLabel: () => i18n.translate('xpack.canvas.toolbar.editorButtonLabel', { diff --git a/x-pack/legacy/plugins/canvas/public/components/page_manager/page_manager.js b/x-pack/legacy/plugins/canvas/public/components/page_manager/page_manager.js index 533733f9aeacf..2208d7f363f5e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/page_manager/page_manager.js +++ b/x-pack/legacy/plugins/canvas/public/components/page_manager/page_manager.js @@ -13,6 +13,10 @@ import { ConfirmModal } from '../confirm_modal'; import { Link } from '../link'; import { PagePreview } from '../page_preview'; +import { ComponentStrings } from '../../../i18n'; + +const { PageManager: strings } = ComponentStrings; + export class PageManager extends React.PureComponent { static propTypes = { isWriteable: PropTypes.bool.isRequired, @@ -154,7 +158,7 @@ export class PageManager extends React.PureComponent { {Style.it( workpadCSS, diff --git a/x-pack/legacy/plugins/canvas/public/components/page_preview/page_controls.js b/x-pack/legacy/plugins/canvas/public/components/page_preview/page_controls.js index f04d44a74165e..727794e72ee15 100644 --- a/x-pack/legacy/plugins/canvas/public/components/page_preview/page_controls.js +++ b/x-pack/legacy/plugins/canvas/public/components/page_preview/page_controls.js @@ -8,6 +8,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { ComponentStrings } from '../../../i18n'; + +const { PagePreviewPageControls: strings } = ComponentStrings; + export const PageControls = ({ pageId, onDelete, onDuplicate }) => { const handleDuplicate = ev => { ev.preventDefault(); @@ -26,16 +30,20 @@ export const PageControls = ({ pageId, onDelete, onDuplicate }) => { justifyContent="spaceBetween" > - - + + - + diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts index 2dcaf35cc41d9..3af07980910b8 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts @@ -6,13 +6,7 @@ import * as rt from 'io-ts'; -import { - badRequestErrorRT, - conflictErrorRT, - forbiddenErrorRT, - metricStatisticsRT, - timeRangeRT, -} from '../../shared'; +import { badRequestErrorRT, conflictErrorRT, forbiddenErrorRT, timeRangeRT } from '../../shared'; export const LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH = '/api/infra/log_analysis/results/log_entry_rate'; @@ -43,12 +37,15 @@ export const logEntryRateAnomaly = rt.type({ typicalLogEntryRate: rt.number, }); -export const logEntryRateHistogramBucket = rt.type({ +export const logEntryRateDataSetRT = rt.type({ + analysisBucketCount: rt.number, anomalies: rt.array(logEntryRateAnomaly), - duration: rt.number, - logEntryRateStats: metricStatisticsRT, - modelLowerBoundStats: metricStatisticsRT, - modelUpperBoundStats: metricStatisticsRT, + averageActualLogEntryRate: rt.number, + dataSetId: rt.string, +}); + +export const logEntryRateHistogramBucket = rt.type({ + dataSets: rt.array(logEntryRateDataSetRT), startTime: rt.number, }); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/get_log_entry_rate.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/get_log_entry_rate.ts new file mode 100644 index 0000000000000..471a00d40984c --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/get_log_entry_rate.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { identity } from 'fp-ts/lib/function'; +import { kfetch } from 'ui/kfetch'; + +import { + getLogEntryRateRequestPayloadRT, + getLogEntryRateSuccessReponsePayloadRT, + LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, +} from '../../../../../common/http_api/log_analysis'; +import { createPlainError, throwErrors } from '../../../../../common/runtime_types'; + +export const callGetLogEntryRateAPI = async ( + sourceId: string, + startTime: number, + endTime: number, + bucketDuration: number +) => { + const response = await kfetch({ + method: 'POST', + pathname: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, + body: JSON.stringify( + getLogEntryRateRequestPayloadRT.encode({ + data: { + sourceId, + timeRange: { + startTime, + endTime, + }, + bucketDuration, + }, + }) + ), + }); + return pipe( + getLogEntryRateSuccessReponsePayloadRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_graph_data/log_entry_rate.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_graph_data/log_entry_rate.tsx deleted file mode 100644 index f54402a1a8707..0000000000000 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_graph_data/log_entry_rate.tsx +++ /dev/null @@ -1,71 +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 { useMemo } from 'react'; -import { GetLogEntryRateSuccessResponsePayload } from '../../../../../common/http_api/log_analysis'; - -interface LogRateAreaSeriesDataPoint { - x: number; - min: number | null; - max: number | null; -} -type LogRateAreaSeries = LogRateAreaSeriesDataPoint[]; -type LogRateLineSeriesDataPoint = [number, number | null]; -type LogRateLineSeries = LogRateLineSeriesDataPoint[]; -type LogRateAnomalySeriesDataPoint = [number, number]; -type LogRateAnomalySeries = LogRateAnomalySeriesDataPoint[]; - -export const useLogEntryRateGraphData = ({ - data, -}: { - data: GetLogEntryRateSuccessResponsePayload['data'] | null; -}) => { - const areaSeries: LogRateAreaSeries = useMemo(() => { - if (!data || (data && data.histogramBuckets && !data.histogramBuckets.length)) { - return []; - } - return data.histogramBuckets.reduce((acc: any, bucket) => { - acc.push({ - x: bucket.startTime, - min: bucket.modelLowerBoundStats.min, - max: bucket.modelUpperBoundStats.max, - }); - return acc; - }, []); - }, [data]); - - const lineSeries: LogRateLineSeries = useMemo(() => { - if (!data || (data && data.histogramBuckets && !data.histogramBuckets.length)) { - return []; - } - return data.histogramBuckets.reduce((acc: any, bucket) => { - acc.push([bucket.startTime, bucket.logEntryRateStats.avg]); - return acc; - }, []); - }, [data]); - - const anomalySeries: LogRateAnomalySeries = useMemo(() => { - if (!data || (data && data.histogramBuckets && !data.histogramBuckets.length)) { - return []; - } - return data.histogramBuckets.reduce((acc: any, bucket) => { - if (bucket.anomalies.length > 0) { - bucket.anomalies.forEach(anomaly => { - acc.push([anomaly.startTime, anomaly.actualLogEntryRate]); - }); - return acc; - } else { - return acc; - } - }, []); - }, [data]); - - return { - areaSeries, - lineSeries, - anomalySeries, - }; -}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx index 4e7a6647a9579..8b21a7e829894 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx @@ -5,19 +5,10 @@ */ import { useMemo, useState } from 'react'; -import { kfetch } from 'ui/kfetch'; -import { fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { identity } from 'fp-ts/lib/function'; -import { - getLogEntryRateRequestPayloadRT, - getLogEntryRateSuccessReponsePayloadRT, - GetLogEntryRateSuccessResponsePayload, - LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, -} from '../../../../common/http_api/log_analysis'; -import { createPlainError, throwErrors } from '../../../../common/runtime_types'; +import { GetLogEntryRateSuccessResponsePayload } from '../../../../common/http_api/log_analysis'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { callGetLogEntryRateAPI } from './api/get_log_entry_rate'; type LogEntryRateResults = GetLogEntryRateSuccessResponsePayload['data']; @@ -38,30 +29,10 @@ export const useLogEntryRate = ({ { cancelPreviousOn: 'resolution', createPromise: async () => { - return await kfetch({ - method: 'POST', - pathname: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, - body: JSON.stringify( - getLogEntryRateRequestPayloadRT.encode({ - data: { - sourceId, - timeRange: { - startTime, - endTime, - }, - bucketDuration, - }, - }) - ), - }); + return await callGetLogEntryRateAPI(sourceId, startTime, endTime, bucketDuration); }, onResolve: response => { - const { data } = pipe( - getLogEntryRateSuccessReponsePayloadRT.decode(response), - fold(throwErrors(createPlainError), identity) - ); - - setLogEntryRate(data); + setLogEntryRate(response.data); }, }, [sourceId, startTime, endTime, bucketDuration] diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/chart_helpers/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/chart_helpers/index.tsx deleted file mode 100644 index df0eca449bb9f..0000000000000 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/chart_helpers/index.tsx +++ /dev/null @@ -1,20 +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 chrome from 'ui/chrome'; -import { SpecId, Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts'; - -export const getColorsMap = (color: string, specId: SpecId) => { - const map = new Map(); - map.set({ colorValues: [], specId }, color); - return map; -}; - -export const isDarkMode = () => chrome.getUiSettingsClient().get('theme:darkMode'); - -export const getChartTheme = (): Theme => { - return isDarkMode() ? DARK_THEME : LIGHT_THEME; -}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx index 3629413d6d30c..aaf24c22594e5 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx @@ -6,7 +6,6 @@ import datemath from '@elastic/datemath'; import { - EuiBadge, EuiFlexGroup, EuiFlexItem, EuiPage, @@ -17,7 +16,6 @@ import { EuiSuperDatePicker, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; import React, { useCallback, useMemo, useState } from 'react'; @@ -122,21 +120,6 @@ export const AnalysisResultsContent = ({ [setAutoRefresh] ); - const anomaliesDetected = useMemo(() => { - if (!logEntryRate) { - return null; - } else { - if (logEntryRate.histogramBuckets && logEntryRate.histogramBuckets.length) { - return logEntryRate.histogramBuckets.reduce( - (acc, bucket) => acc + bucket.anomalies.length, - 0 - ); - } else { - return null; - } - } - }, [logEntryRate]); - return ( <> {isLoading && !logEntryRate ? ( @@ -150,29 +133,8 @@ export const AnalysisResultsContent = ({ - - - - {anomaliesDetected !== null ? ( - - - {anomaliesDetected} - - ), - number: anomaliesDetected, - }} - /> - - ) : null} - - - - + + void; + timeRange: TimeRange; +}> = ({ bucketDuration, histogramBuckets, setTimeRange, timeRange }) => { + const [dateFormat] = useKibanaUiSetting('dateFormat'); + const [isDarkMode] = useKibanaUiSetting('theme:darkMode'); + + const chartDateFormatter = useMemo( + () => niceTimeFormatter([timeRange.startTime, timeRange.endTime]), + [timeRange] + ); + + const logEntryRateSeries = useMemo( + () => + histogramBuckets + ? histogramBuckets.reduce>( + (buckets, bucket) => { + return [ + ...buckets, + ...bucket.dataSets.map(dataSet => ({ + group: dataSet.dataSetId === '' ? 'unknown' : dataSet.dataSetId, + time: bucket.startTime, + value: dataSet.averageActualLogEntryRate, + })), + ]; + }, + [] + ) + : [], + [histogramBuckets] + ); + + const logEntryRateAnomalyAnnotations = useMemo( + () => + histogramBuckets + ? histogramBuckets.reduce((annotatedBuckets, bucket) => { + const anomalies = bucket.dataSets.reduce( + (accumulatedAnomalies, dataSet) => [...accumulatedAnomalies, ...dataSet.anomalies], + [] + ); + if (anomalies.length <= 0) { + return annotatedBuckets; + } + return [ + ...annotatedBuckets, + { + coordinates: { + x0: bucket.startTime, + x1: bucket.startTime + bucketDuration, + }, + details: i18n.translate( + 'xpack.infra.logs.analysis.logRateSectionAnomalyCountTooltipLabel', + { + defaultMessage: `{anomalyCount, plural, one {# anomaly} other {# anomalies}}`, + values: { + anomalyCount: anomalies.length, + }, + } + ), + }, + ]; + }, []) + : [], + [histogramBuckets] + ); + + const logEntryRateSpecId = getSpecId('averageValues'); + const logEntryRateAnomalyAnnotationsId = getAnnotationId('anomalies'); + + const tooltipProps = useMemo( + () => ({ + headerFormatter: (tooltipData: TooltipValue) => + moment(tooltipData.value).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'), + }), + [dateFormat] + ); + + const handleBrushEnd = useCallback( + (startTime: number, endTime: number) => { + setTimeRange({ + endTime, + startTime, + }); + }, + [setTimeRange] + ); + + return ( +
+ + + Number(value).toFixed(0)} + /> + + + + +
+ ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/chart.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/chart.tsx deleted file mode 100644 index 0d703420e7412..0000000000000 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/chart.tsx +++ /dev/null @@ -1,196 +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 React, { useMemo, useCallback, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import moment from 'moment'; -import { - Axis, - Chart, - getAxisId, - getSpecId, - AreaSeries, - LineSeries, - niceTimeFormatter, - Settings, - TooltipValue, -} from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui'; -import { getColorsMap, isDarkMode, getChartTheme } from '../../chart_helpers'; -import { GetLogEntryRateSuccessResponsePayload } from '../../../../../../common/http_api/log_analysis/results/log_entry_rate'; -import { useLogEntryRateGraphData } from '../../../../../containers/logs/log_analysis/log_analysis_graph_data/log_entry_rate'; -import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting'; -import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; - -const areaSeriesColour = 'rgb(224, 237, 255)'; -const lineSeriesColour = 'rgb(49, 133, 252)'; - -interface Props { - data: GetLogEntryRateSuccessResponsePayload['data'] | null; - setTimeRange: (timeRange: TimeRange) => void; - timeRange: TimeRange; -} - -export const ChartView = ({ data, setTimeRange, timeRange }: Props) => { - const { areaSeries, lineSeries, anomalySeries } = useLogEntryRateGraphData({ data }); - - const dateFormatter = useMemo( - () => - lineSeries.length > 0 - ? niceTimeFormatter([timeRange.startTime, timeRange.endTime]) - : (value: number) => `${value}`, - [lineSeries, timeRange] - ); - - const areaSpecId = getSpecId('modelBounds'); - const lineSpecId = getSpecId('averageValues'); - const anomalySpecId = getSpecId('anomalies'); - - const [dateFormat] = useKibanaUiSetting('dateFormat'); - - const tooltipProps = useMemo( - () => ({ - headerFormatter: (tooltipData: TooltipValue) => - moment(tooltipData.value).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'), - }), - [dateFormat] - ); - - const [isShowingModelBounds, setIsShowingModelBounds] = useState(true); - - const handleBrushEnd = useCallback( - (startTime: number, endTime: number) => { - setTimeRange({ - endTime, - startTime, - }); - }, - [setTimeRange] - ); - - return ( - <> - - - { - setIsShowingModelBounds(e.target.checked); - }} - /> - - -
- - - Number(value).toFixed(0)} - /> - {isShowingModelBounds ? ( - - ) : null} - - - - -
- - ); -}; - -const showModelBoundsLabel = i18n.translate( - 'xpack.infra.logs.analysis.logRateSectionModelBoundsCheckboxLabel', - { defaultMessage: 'Show model bounds' } -); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/index.tsx index c03a4817e4d4c..1f01af33e33c4 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/index.tsx @@ -16,8 +16,8 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { GetLogEntryRateSuccessResponsePayload } from '../../../../../../common/http_api/log_analysis/results/log_entry_rate'; -import { ChartView } from './chart'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; +import { LogEntryRateBarChart } from './bar_chart'; export const LogRateResults = ({ isLoading, @@ -70,7 +70,12 @@ export const LogRateResults = ({ } /> ) : ( - + )} ); diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts index ac7f7b6df8d62..31d9c5403e2d2 100644 --- a/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as rt from 'io-ts'; - import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; @@ -13,8 +11,14 @@ import { getJobId } from '../../../common/log_analysis'; import { throwErrors, createPlainError } from '../../../common/runtime_types'; import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../adapters/framework'; import { NoLogRateResultsIndexError } from './errors'; +import { + logRateModelPlotResponseRT, + createLogEntryRateQuery, + LogRateModelPlotBucket, + CompositeTimestampDataSetKey, +} from './queries'; -const ML_ANOMALY_INDEX_PREFIX = '.ml-anomalies-'; +const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; export class InfraLogAnalysis { constructor( @@ -38,168 +42,95 @@ export class InfraLogAnalysis { ) { const logRateJobId = this.getJobIds(request, sourceId).logEntryRate; - const mlModelPlotResponse = await this.libs.framework.callWithRequest(request, 'search', { - allowNoIndices: true, - body: { - query: { - bool: { - filter: [ - { - range: { - timestamp: { - gte: startTime, - lt: endTime, - }, - }, - }, - { - terms: { - result_type: ['model_plot', 'record'], - }, - }, - { - term: { - detector_index: { - value: 0, - }, - }, - }, - ], - }, - }, - aggs: { - timestamp_buckets: { - date_histogram: { - field: 'timestamp', - fixed_interval: `${bucketDuration}ms`, - }, - aggs: { - filter_model_plot: { - filter: { - term: { - result_type: 'model_plot', - }, - }, - aggs: { - stats_model_lower: { - stats: { - field: 'model_lower', - }, - }, - stats_model_upper: { - stats: { - field: 'model_upper', - }, - }, - stats_actual: { - stats: { - field: 'actual', - }, - }, - }, - }, - filter_records: { - filter: { - term: { - result_type: 'record', - }, - }, - aggs: { - top_hits_record: { - top_hits: { - _source: Object.keys(logRateMlRecordRT.props), - size: 100, - sort: [ - { - timestamp: 'asc', - }, - ], - }, - }, - }, - }, - }, - }, - }, - }, - ignoreUnavailable: true, - index: `${ML_ANOMALY_INDEX_PREFIX}${logRateJobId}`, - size: 0, - trackScores: false, - trackTotalHits: false, - }); + let mlModelPlotBuckets: LogRateModelPlotBucket[] = []; + let afterLatestBatchKey: CompositeTimestampDataSetKey | undefined; - if (mlModelPlotResponse._shards.total === 0) { - throw new NoLogRateResultsIndexError( - `Failed to find ml result index for job ${logRateJobId}.` + while (true) { + const mlModelPlotResponse = await this.libs.framework.callWithRequest( + request, + 'search', + createLogEntryRateQuery( + logRateJobId, + startTime, + endTime, + bucketDuration, + COMPOSITE_AGGREGATION_BATCH_SIZE, + afterLatestBatchKey + ) ); - } - const mlModelPlotBuckets = pipe( - logRateModelPlotResponseRT.decode(mlModelPlotResponse), - map(response => response.aggregations.timestamp_buckets.buckets), - fold(throwErrors(createPlainError), identity) - ); + if (mlModelPlotResponse._shards.total === 0) { + throw new NoLogRateResultsIndexError( + `Failed to find ml result index for job ${logRateJobId}.` + ); + } - return mlModelPlotBuckets.map(bucket => ({ - anomalies: bucket.filter_records.top_hits_record.hits.hits.map(({ _source: record }) => ({ - actualLogEntryRate: record.actual[0], - anomalyScore: record.record_score, - duration: record.bucket_span * 1000, - startTime: record.timestamp, - typicalLogEntryRate: record.typical[0], - })), - duration: bucketDuration, - logEntryRateStats: bucket.filter_model_plot.stats_actual, - modelLowerBoundStats: bucket.filter_model_plot.stats_model_lower, - modelUpperBoundStats: bucket.filter_model_plot.stats_model_upper, - startTime: bucket.key, - })); - } -} + const { after_key: afterKey, buckets: latestBatchBuckets } = pipe( + logRateModelPlotResponseRT.decode(mlModelPlotResponse), + map(response => response.aggregations.timestamp_data_set_buckets), + fold(throwErrors(createPlainError), identity) + ); -const logRateMlRecordRT = rt.type({ - actual: rt.array(rt.number), - bucket_span: rt.number, - record_score: rt.number, - timestamp: rt.number, - typical: rt.array(rt.number), -}); + mlModelPlotBuckets = [...mlModelPlotBuckets, ...latestBatchBuckets]; + afterLatestBatchKey = afterKey; -const logRateStatsAggregationRT = rt.type({ - avg: rt.union([rt.number, rt.null]), - count: rt.number, - max: rt.union([rt.number, rt.null]), - min: rt.union([rt.number, rt.null]), - sum: rt.number, -}); + if (latestBatchBuckets.length < COMPOSITE_AGGREGATION_BATCH_SIZE) { + break; + } + } -const logRateModelPlotResponseRT = rt.type({ - aggregations: rt.type({ - timestamp_buckets: rt.type({ - buckets: rt.array( - rt.type({ - key: rt.number, - filter_records: rt.type({ - doc_count: rt.number, - top_hits_record: rt.type({ - hits: rt.type({ - hits: rt.array( - rt.type({ - _source: logRateMlRecordRT, - }) - ), - }), - }), - }), - filter_model_plot: rt.type({ - doc_count: rt.number, - stats_actual: logRateStatsAggregationRT, - stats_model_lower: logRateStatsAggregationRT, - stats_model_upper: logRateStatsAggregationRT, - }), - }) - ), - }), - }), -}); + return mlModelPlotBuckets.reduce< + Array<{ + dataSets: Array<{ + analysisBucketCount: number; + anomalies: Array<{ + actualLogEntryRate: number; + anomalyScore: number; + duration: number; + startTime: number; + typicalLogEntryRate: number; + }>; + averageActualLogEntryRate: number; + dataSetId: string; + }>; + startTime: number; + }> + >((histogramBuckets, timestampDataSetBucket) => { + const previousHistogramBucket = histogramBuckets[histogramBuckets.length - 1]; + const dataSet = { + analysisBucketCount: timestampDataSetBucket.filter_model_plot.doc_count, + anomalies: timestampDataSetBucket.filter_records.top_hits_record.hits.hits.map( + ({ _source: record }) => ({ + actualLogEntryRate: record.actual[0], + anomalyScore: record.record_score, + duration: record.bucket_span * 1000, + startTime: record.timestamp, + typicalLogEntryRate: record.typical[0], + }) + ), + averageActualLogEntryRate: timestampDataSetBucket.filter_model_plot.average_actual.value, + dataSetId: timestampDataSetBucket.key.data_set, + }; + if ( + previousHistogramBucket && + previousHistogramBucket.startTime === timestampDataSetBucket.key.timestamp + ) { + return [ + ...histogramBuckets.slice(0, -1), + { + ...previousHistogramBucket, + dataSets: [...previousHistogramBucket.dataSets, dataSet], + }, + ]; + } else { + return [ + ...histogramBuckets, + { + dataSets: [dataSet], + startTime: timestampDataSetBucket.key.timestamp, + }, + ]; + } + }, []); + } +} diff --git a/x-pack/legacy/plugins/uptime/public/apps/kibana_app.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/index.ts similarity index 67% rename from x-pack/legacy/plugins/uptime/public/apps/kibana_app.ts rename to x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/index.ts index bc82dc0cddeec..1749421277719 100644 --- a/x-pack/legacy/plugins/uptime/public/apps/kibana_app.ts +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { compose } from '../lib/compose/kibana_compose'; -import { startApp } from './start_app'; - -startApp(compose()); +export * from './log_entry_rate'; diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts new file mode 100644 index 0000000000000..b10b1fe04db24 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts @@ -0,0 +1,170 @@ +/* + * 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 * as rt from 'io-ts'; + +const ML_ANOMALY_INDEX_PREFIX = '.ml-anomalies-'; + +export const createLogEntryRateQuery = ( + logRateJobId: string, + startTime: number, + endTime: number, + bucketDuration: number, + size: number, + afterKey?: CompositeTimestampDataSetKey +) => ({ + allowNoIndices: true, + body: { + query: { + bool: { + filter: [ + { + range: { + timestamp: { + gte: startTime, + lt: endTime, + }, + }, + }, + { + terms: { + result_type: ['model_plot', 'record'], + }, + }, + { + term: { + detector_index: { + value: 0, + }, + }, + }, + ], + }, + }, + aggs: { + timestamp_data_set_buckets: { + composite: { + after: afterKey, + size, + sources: [ + { + timestamp: { + date_histogram: { + field: 'timestamp', + fixed_interval: `${bucketDuration}ms`, + order: 'asc', + }, + }, + }, + { + data_set: { + terms: { + field: 'partition_field_value', + order: 'asc', + }, + }, + }, + ], + }, + aggs: { + filter_model_plot: { + filter: { + term: { + result_type: 'model_plot', + }, + }, + aggs: { + average_actual: { + avg: { + field: 'actual', + }, + }, + }, + }, + filter_records: { + filter: { + term: { + result_type: 'record', + }, + }, + aggs: { + top_hits_record: { + top_hits: { + _source: Object.keys(logRateMlRecordRT.props), + size: 100, + sort: [ + { + timestamp: 'asc', + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, + ignoreUnavailable: true, + index: `${ML_ANOMALY_INDEX_PREFIX}${logRateJobId}`, + size: 0, + trackScores: false, + trackTotalHits: false, +}); + +const logRateMlRecordRT = rt.type({ + actual: rt.array(rt.number), + bucket_span: rt.number, + record_score: rt.number, + timestamp: rt.number, + typical: rt.array(rt.number), +}); + +const metricAggregationRT = rt.type({ + value: rt.number, +}); + +const compositeTimestampDataSetKeyRT = rt.type({ + data_set: rt.string, + timestamp: rt.number, +}); + +export type CompositeTimestampDataSetKey = rt.TypeOf; + +export const logRateModelPlotBucketRT = rt.type({ + key: compositeTimestampDataSetKeyRT, + filter_records: rt.type({ + doc_count: rt.number, + top_hits_record: rt.type({ + hits: rt.type({ + hits: rt.array( + rt.type({ + _source: logRateMlRecordRT, + }) + ), + }), + }), + }), + filter_model_plot: rt.type({ + doc_count: rt.number, + average_actual: metricAggregationRT, + }), +}); + +export type LogRateModelPlotBucket = rt.TypeOf; + +export const logRateModelPlotResponseRT = rt.type({ + aggregations: rt.type({ + timestamp_data_set_buckets: rt.intersection([ + rt.type({ + buckets: rt.array(logRateModelPlotBucketRT), + }), + rt.partial({ + after_key: compositeTimestampDataSetKeyRT, + }), + ]), + }), +}); diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index 399a65041b664..c64757de5fb0a 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -65,17 +65,17 @@ export const lens: LegacyPluginInitializer = kibana => { api: [PLUGIN_ID], catalogue: [PLUGIN_ID], savedObject: { - all: [], - read: [], + all: ['search'], + read: ['index-pattern'], }, - ui: ['save', 'show'], + ui: ['save', 'show', 'saveQuery'], }, read: { api: [PLUGIN_ID], catalogue: [PLUGIN_ID], savedObject: { all: [], - read: [], + read: ['index-pattern'], }, ui: ['show'], }, diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/_app.scss b/x-pack/legacy/plugins/lens/public/app_plugin/_app.scss index 382a3f5522daf..ed3a178cdd5ea 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/_app.scss +++ b/x-pack/legacy/plugins/lens/public/app_plugin/_app.scss @@ -11,7 +11,6 @@ } .lnsApp__header { - padding: $euiSize; border-bottom: $euiBorderThin; } diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index a8df5eafe71ff..103697ef9148a 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -5,19 +5,23 @@ */ import React from 'react'; +import { ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { buildExistsFilter } from '@kbn/es-query'; import { App } from './app'; import { EditorFrameInstance } from '../types'; import { Storage } from 'ui/storage'; import { Document, SavedObjectStore } from '../persistence'; import { mount } from 'enzyme'; -import { QueryBarTopRow } from '../../../../../../src/legacy/core_plugins/data/public/query/query_bar'; -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { + TopNavMenu, + TopNavMenuData, +} from '../../../../../../src/legacy/core_plugins/kibana_react/public'; +import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public'; import { coreMock } from 'src/core/public/mocks'; -const dataStartMock = dataPluginMock.createStartContract(); - -jest.mock('../../../../../../src/legacy/core_plugins/data/public/query/query_bar', () => ({ - QueryBarTopRow: jest.fn(() => null), +jest.mock('../../../../../../src/legacy/core_plugins/kibana_react/public', () => ({ + TopNavMenu: jest.fn(() => null), })); jest.mock('ui/new_platform'); @@ -33,14 +37,39 @@ function createMockFrame(): jest.Mocked { }; } +function createMockFilterManager() { + const unsubscribe = jest.fn(); + + let subscriber: () => void; + let filters: unknown = []; + + return { + getUpdates$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + setFilters: (newFilters: unknown[]) => { + filters = newFilters; + subscriber(); + }, + getFilters: () => filters, + removeAll: () => { + filters = []; + subscriber(); + }, + }; +} + describe('Lens App', () => { let frame: jest.Mocked; let core: ReturnType; function makeDefaultArgs(): jest.Mocked<{ editorFrame: EditorFrameInstance; - data: typeof dataStartMock; core: typeof core; + data: DataStart; store: Storage; docId?: string; docStorage: SavedObjectStore; @@ -48,8 +77,29 @@ describe('Lens App', () => { }> { return ({ editorFrame: createMockFrame(), - core, - data: dataStartMock, + core: { + ...core, + application: { + ...core.application, + capabilities: { + ...core.application.capabilities, + lens: { save: true, saveQuery: true, show: true }, + }, + }, + }, + data: { + indexPatterns: { + indexPatterns: { + get: jest.fn(id => { + return new Promise(resolve => resolve({ id })); + }), + }, + }, + timefilter: { history: {} }, + filter: { + filterManager: createMockFilterManager(), + }, + }, store: { get: jest.fn(), }, @@ -57,13 +107,11 @@ describe('Lens App', () => { load: jest.fn(), save: jest.fn(), }, - QueryBarTopRow: jest.fn(() =>
), redirectTo: jest.fn(id => {}), - savedObjectsClient: jest.fn(), } as unknown) as jest.Mocked<{ editorFrame: EditorFrameInstance; - data: typeof dataStartMock; core: typeof core; + data: DataStart; store: Storage; docId?: string; docStorage: SavedObjectStore; @@ -109,12 +157,14 @@ describe('Lens App', () => { "toDate": "now", }, "doc": undefined, + "filters": Array [], "onChange": [Function], "onError": [Function], "query": Object { "language": "kuery", "query": "", }, + "savedQuery": undefined, }, ], ] @@ -174,12 +224,11 @@ describe('Lens App', () => { await waitForPromises(); expect(args.docStorage.load).toHaveBeenCalledWith('1234'); - expect(QueryBarTopRow).toHaveBeenCalledWith( + expect(args.data.indexPatterns.indexPatterns.get).toHaveBeenCalledWith('1'); + expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ - dateRangeFrom: 'now-7d', - dateRangeTo: 'now', query: 'fake query', - indexPatterns: ['saved'], + indexPatterns: [{ id: '1' }], }), {} ); @@ -233,30 +282,51 @@ describe('Lens App', () => { }); describe('save button', () => { - it('shows a save button that is enabled when the frame has provided its state', () => { + function getButton(instance: ReactWrapper): TopNavMenuData { + return (instance + .find('[data-test-subj="lnsApp_topNav"]') + .prop('config') as TopNavMenuData[]).find( + button => button.testId === 'lnsApp_saveButton' + )!; + } + + it('shows a disabled save button when the user does not have permissions', async () => { + const args = makeDefaultArgs(); + args.core.application = { + ...args.core.application, + capabilities: { + ...args.core.application.capabilities, + lens: { save: false, saveQuery: false, show: true }, + }, + }; + args.editorFrame = frame; + + const instance = mount(); + + expect(getButton(instance).disableButton).toEqual(true); + + const onChange = frame.mount.mock.calls[0][1].onChange; + onChange({ filterableIndexPatterns: [], doc: ('will save this' as unknown) as Document }); + + instance.update(); + + expect(getButton(instance).disableButton).toEqual(true); + }); + + it('shows a save button that is enabled when the frame has provided its state', async () => { const args = makeDefaultArgs(); args.editorFrame = frame; const instance = mount(); - expect( - instance - .find('[data-test-subj="lnsApp_saveButton"]') - .first() - .prop('disabled') - ).toEqual(true); + expect(getButton(instance).disableButton).toEqual(true); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ indexPatternTitles: [], doc: ('will save this' as unknown) as Document }); + onChange({ filterableIndexPatterns: [], doc: ('will save this' as unknown) as Document }); instance.update(); - expect( - instance - .find('[data-test-subj="lnsApp_saveButton"]') - .first() - .prop('disabled') - ).toEqual(false); + expect(getButton(instance).disableButton).toEqual(false); }); it('saves the latest doc and then prevents more saving', async () => { @@ -269,21 +339,15 @@ describe('Lens App', () => { expect(frame.mount).toHaveBeenCalledTimes(1); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ indexPatternTitles: [], doc: ({ id: undefined } as unknown) as Document }); + onChange({ filterableIndexPatterns: [], doc: ({ id: undefined } as unknown) as Document }); instance.update(); - expect( - instance - .find('[data-test-subj="lnsApp_saveButton"]') - .first() - .prop('disabled') - ).toEqual(false); + expect(getButton(instance).disableButton).toEqual(false); - instance - .find('[data-test-subj="lnsApp_saveButton"]') - .first() - .prop('onClick')!({} as React.MouseEvent); + act(() => { + getButton(instance).run(instance.getDOMNode()); + }); expect(args.docStorage.save).toHaveBeenCalledWith({ id: undefined }); @@ -295,12 +359,7 @@ describe('Lens App', () => { expect(args.docStorage.load).not.toHaveBeenCalled(); - expect( - instance - .find('[data-test-subj="lnsApp_saveButton"]') - .first() - .prop('disabled') - ).toEqual(true); + expect(getButton(instance).disableButton).toEqual(true); }); it('handles save failure by showing a warning, but still allows another save', async () => { @@ -311,27 +370,22 @@ describe('Lens App', () => { const instance = mount(); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ indexPatternTitles: [], doc: ({ id: undefined } as unknown) as Document }); + onChange({ filterableIndexPatterns: [], doc: ({ id: undefined } as unknown) as Document }); instance.update(); - instance - .find('[data-test-subj="lnsApp_saveButton"]') - .first() - .prop('onClick')!({} as React.MouseEvent); + act(() => { + getButton(instance).run(instance.getDOMNode()); + }); + await waitForPromises(); await waitForPromises(); expect(args.core.notifications.toasts.addDanger).toHaveBeenCalled(); expect(args.redirectTo).not.toHaveBeenCalled(); await waitForPromises(); - expect( - instance - .find('[data-test-subj="lnsApp_saveButton"]') - .first() - .prop('disabled') - ).toEqual(false); + expect(getButton(instance).disableButton).toEqual(false); }); }); }); @@ -343,10 +397,8 @@ describe('Lens App', () => { mount(); - expect(QueryBarTopRow).toHaveBeenCalledWith( + expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ - dateRangeFrom: 'now-7d', - dateRangeTo: 'now', query: { query: '', language: 'kuery' }, }), {} @@ -360,13 +412,13 @@ describe('Lens App', () => { ); }); - it('updates the index patterns when the editor frame is changed', () => { + it('updates the index patterns when the editor frame is changed', async () => { const args = makeDefaultArgs(); args.editorFrame = frame; const instance = mount(); - expect(QueryBarTopRow).toHaveBeenCalledWith( + expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ indexPatterns: [], }), @@ -375,40 +427,52 @@ describe('Lens App', () => { const onChange = frame.mount.mock.calls[0][1].onChange; onChange({ - indexPatternTitles: ['newIndex'], + filterableIndexPatterns: [{ id: '1', title: 'newIndex' }], + doc: ({ id: undefined } as unknown) as Document, + }); + + await waitForPromises(); + instance.update(); + + expect(TopNavMenu).toHaveBeenCalledWith( + expect.objectContaining({ + indexPatterns: [{ id: '1' }], + }), + {} + ); + + // Do it again to verify that the dirty checking is done right + onChange({ + filterableIndexPatterns: [{ id: '2', title: 'second index' }], doc: ({ id: undefined } as unknown) as Document, }); + await waitForPromises(); instance.update(); - expect(QueryBarTopRow).toHaveBeenCalledWith( + expect(TopNavMenu).toHaveBeenLastCalledWith( expect.objectContaining({ - indexPatterns: ['newIndex'], + indexPatterns: [{ id: '2' }], }), {} ); }); - it('updates the editor frame when the user changes query or time', () => { + it('updates the editor frame when the user changes query or time in the search bar', () => { const args = makeDefaultArgs(); args.editorFrame = frame; const instance = mount(); - instance - .find('[data-test-subj="lnsApp_queryBar"]') - .first() - .prop('onSubmit')!(({ + instance.find(TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, - } as unknown) as React.FormEvent); + }); instance.update(); - expect(QueryBarTopRow).toHaveBeenCalledWith( + expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ - dateRangeFrom: 'now-14d', - dateRangeTo: 'now-7d', query: { query: 'new', language: 'lucene' }, }), {} @@ -421,6 +485,159 @@ describe('Lens App', () => { }) ); }); + + it('updates the filters when the user changes them', () => { + const args = makeDefaultArgs(); + args.editorFrame = frame; + + const instance = mount(); + + args.data.filter.filterManager.setFilters([ + buildExistsFilter({ name: 'myfield' }, { id: 'index1' }), + ]); + + instance.update(); + + expect(frame.mount).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + filters: [buildExistsFilter({ name: 'myfield' }, { id: 'index1' })], + }) + ); + }); + }); + + describe('saved query handling', () => { + it('does not allow saving when the user is missing the saveQuery permission', () => { + const args = makeDefaultArgs(); + args.core.application = { + ...args.core.application, + capabilities: { + ...args.core.application.capabilities, + lens: { save: false, saveQuery: false, show: true }, + }, + }; + + mount(); + + expect(TopNavMenu).toHaveBeenCalledWith( + expect.objectContaining({ showSaveQuery: false }), + {} + ); + }); + + it('persists the saved query ID when the query is saved', () => { + const args = makeDefaultArgs(); + args.editorFrame = frame; + + const instance = mount(); + + expect(TopNavMenu).toHaveBeenCalledWith( + expect.objectContaining({ + showSaveQuery: true, + savedQuery: undefined, + onSaved: expect.any(Function), + onSavedQueryUpdated: expect.any(Function), + onClearSavedQuery: expect.any(Function), + }), + {} + ); + + act(() => { + instance.find(TopNavMenu).prop('onSaved')!({ + id: '1', + attributes: { + title: '', + description: '', + query: { query: '', language: 'lucene' }, + }, + }); + }); + + expect(TopNavMenu).toHaveBeenCalledWith( + expect.objectContaining({ + savedQuery: { + id: '1', + attributes: { + title: '', + description: '', + query: { query: '', language: 'lucene' }, + }, + }, + }), + {} + ); + }); + + it('changes the saved query ID when the query is updated', () => { + const args = makeDefaultArgs(); + args.editorFrame = frame; + + const instance = mount(); + + act(() => { + instance.find(TopNavMenu).prop('onSaved')!({ + id: '1', + attributes: { + title: '', + description: '', + query: { query: '', language: 'lucene' }, + }, + }); + }); + + act(() => { + instance.find(TopNavMenu).prop('onSavedQueryUpdated')!({ + id: '2', + attributes: { + title: 'new title', + description: '', + query: { query: '', language: 'lucene' }, + }, + }); + }); + + expect(TopNavMenu).toHaveBeenCalledWith( + expect.objectContaining({ + savedQuery: { + id: '2', + attributes: { + title: 'new title', + description: '', + query: { query: '', language: 'lucene' }, + }, + }, + }), + {} + ); + }); + + it('clears all existing filters when the active saved query is cleared', () => { + const args = makeDefaultArgs(); + args.editorFrame = frame; + + const instance = mount(); + + instance.find(TopNavMenu).prop('onQuerySubmit')!({ + dateRange: { from: 'now-14d', to: 'now-7d' }, + query: { query: 'new', language: 'lucene' }, + }); + + args.data.filter.filterManager.setFilters([ + buildExistsFilter({ name: 'myfield' }, { id: 'index1' }), + ]); + instance.update(); + + instance.find(TopNavMenu).prop('onClearSavedQuery')!(); + instance.update(); + + expect(frame.mount).toHaveBeenLastCalledWith( + expect.any(Element), + expect.objectContaining({ + filters: [], + }) + ); + }); }); it('displays errors from the frame in a toast', () => { diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index a91462171abf7..f41d09c41166f 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -8,12 +8,17 @@ import _ from 'lodash'; import React, { useState, useEffect, useCallback, useRef } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Storage } from 'ui/storage'; -import { CoreStart } from 'src/core/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { Query } from '../../../../../../src/legacy/core_plugins/data/public'; -import { QueryBarTopRow } from '../../../../../../src/legacy/core_plugins/data/public/query/query_bar'; +import { CoreStart, NotificationsStart } from 'src/core/public'; +import { + DataStart, + IndexPattern as IndexPatternInstance, + IndexPatterns as IndexPatternsService, + SavedQuery, + Query, +} from 'src/legacy/core_plugins/data/public'; +import { Filter } from '@kbn/es-query'; +import { TopNavMenu } from '../../../../../../src/legacy/core_plugins/kibana_react/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Document, SavedObjectStore } from '../persistence'; import { EditorFrameInstance } from '../types'; @@ -22,32 +27,17 @@ import { NativeRenderer } from '../native_renderer'; interface State { isLoading: boolean; isDirty: boolean; + indexPatternsForTopNav: IndexPatternInstance[]; + persistedDoc?: Document; + + // Properties needed to interface with TopNav dateRange: { fromDate: string; toDate: string; }; query: Query; - indexPatternTitles: string[]; - persistedDoc?: Document; - localQueryBarState: { - query?: Query; - dateRange?: { - from: string; - to: string; - }; - }; -} - -function isLocalStateDirty( - localState: State['localQueryBarState'], - query: Query, - dateRange: State['dateRange'] -) { - return Boolean( - (localState.query && query && localState.query.query !== query.query) || - (localState.dateRange && dateRange.fromDate !== localState.dateRange.from) || - (localState.dateRange && dateRange.toDate !== localState.dateRange.to) - ); + filters: Filter[]; + savedQuery?: SavedQuery; } export function App({ @@ -60,8 +50,8 @@ export function App({ redirectTo, }: { editorFrame: EditorFrameInstance; - data: DataPublicPluginStart; core: CoreStart; + data: DataStart; store: Storage; docId?: string; docStorage: SavedObjectStore; @@ -74,23 +64,29 @@ export function App({ const [state, setState] = useState({ isLoading: !!docId, isDirty: false, + indexPatternsForTopNav: [], + query: { query: '', language }, dateRange: { fromDate: timeDefaults.from, toDate: timeDefaults.to, }, - indexPatternTitles: [], - localQueryBarState: { - query: { query: '', language }, - dateRange: { - from: timeDefaults.from, - to: timeDefaults.to, - }, - }, + filters: [], }); const lastKnownDocRef = useRef(undefined); + useEffect(() => { + const subscription = data.filter.filterManager.getUpdates$().subscribe({ + next: () => { + setState(s => ({ ...s, filters: data.filter.filterManager.getFilters() })); + }, + }); + return () => { + subscription.unsubscribe(); + }; + }, []); + // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { core.chrome.setBreadcrumbs([ @@ -110,26 +106,34 @@ export function App({ useEffect(() => { if (docId && (!state.persistedDoc || state.persistedDoc.id !== docId)) { - setState({ ...state, isLoading: true }); + setState(s => ({ ...s, isLoading: true })); docStorage .load(docId) .then(doc => { - setState({ - ...state, - isLoading: false, - persistedDoc: doc, - query: doc.state.query, - localQueryBarState: { - ...state.localQueryBarState, - query: doc.state.query, - }, - indexPatternTitles: doc.state.datasourceMetaData.filterableIndexPatterns.map( - ({ title }) => title - ), - }); + getAllIndexPatterns( + doc.state.datasourceMetaData.filterableIndexPatterns, + data.indexPatterns.indexPatterns, + core.notifications + ) + .then(indexPatterns => { + setState(s => ({ + ...s, + isLoading: false, + persistedDoc: doc, + query: doc.state.query, + filters: doc.state.filters, + dateRange: doc.state.dateRange || s.dateRange, + indexPatternsForTopNav: indexPatterns, + })); + }) + .catch(() => { + setState(s => ({ ...s, isLoading: false })); + + redirectTo(); + }); }) .catch(() => { - setState({ ...state, isLoading: false }); + setState(s => ({ ...s, isLoading: false })); core.notifications.toasts.addDanger( i18n.translate('xpack.lens.editorFrame.docLoadingError', { @@ -145,7 +149,7 @@ export function App({ // Can save if the frame has told us what it has, and there is either: // a) No saved doc // b) A saved doc that differs from the frame state - const isSaveable = state.isDirty; + const isSaveable = state.isDirty && (core.application.capabilities.lens.save as boolean); const onError = useCallback( (e: { message: string }) => @@ -167,76 +171,95 @@ export function App({ >
- - { + if (isSaveable && lastKnownDocRef.current) { + docStorage + .save(lastKnownDocRef.current) + .then(({ id }) => { + // Prevents unnecessary network request and disables save button + const newDoc = { ...lastKnownDocRef.current!, id }; + setState(s => ({ + ...s, + isDirty: false, + persistedDoc: newDoc, + })); + if (docId !== id) { + redirectTo(id); + } + }) + .catch(() => { + core.notifications.toasts.addDanger( + i18n.translate('xpack.lens.editorFrame.docSavingError', { + defaultMessage: 'Error saving document', + }) + ); + }); + } + }, + testId: 'lnsApp_saveButton', + disableButton: !isSaveable, + }, + ]} + data-test-subj="lnsApp_topNav" screenTitle={'lens'} - onSubmit={payload => { + onQuerySubmit={payload => { const { dateRange, query } = payload; - setState({ - ...state, + setState(s => ({ + ...s, dateRange: { fromDate: dateRange.from, toDate: dateRange.to, }, - query: query || state.query, - localQueryBarState: payload, - }); - }} - onChange={localQueryBarState => { - setState({ ...state, localQueryBarState }); + query: query || s.query, + })); }} - isDirty={isLocalStateDirty(state.localQueryBarState, state.query, state.dateRange)} - indexPatterns={state.indexPatternTitles} + appName={'lens'} + indexPatterns={state.indexPatternsForTopNav} + showSearchBar={true} showDatePicker={true} - showQueryInput={true} - query={state.localQueryBarState.query} - dateRangeFrom={ - state.localQueryBarState.dateRange && state.localQueryBarState.dateRange.from - } - dateRangeTo={ - state.localQueryBarState.dateRange && state.localQueryBarState.dateRange.to - } + showQueryBar={true} + showFilterBar={true} + showSaveQuery={core.application.capabilities.lens.saveQuery as boolean} + savedQuery={state.savedQuery} + onSaved={savedQuery => { + setState(s => ({ ...s, savedQuery })); + }} + onSavedQueryUpdated={savedQuery => { + data.filter.filterManager.setFilters( + savedQuery.attributes.filters || state.filters + ); + setState(s => ({ + ...s, + savedQuery: { ...savedQuery }, // Shallow query for reference issues + dateRange: savedQuery.attributes.timefilter + ? { + fromDate: savedQuery.attributes.timefilter.from, + toDate: savedQuery.attributes.timefilter.to, + } + : s.dateRange, + })); + }} + onClearSavedQuery={() => { + data.filter.filterManager.removeAll(); + setState(s => ({ + ...s, + savedQuery: undefined, + filters: [], + query: { + query: '', + language: + store.get('kibana.userQueryLanguage') || + core.uiSettings.get('search:queryLanguage'), + }, + })); + }} + query={state.query} />
@@ -247,22 +270,35 @@ export function App({ nativeProps={{ dateRange: state.dateRange, query: state.query, + filters: state.filters, + savedQuery: state.savedQuery, doc: state.persistedDoc, onError, - onChange: ({ indexPatternTitles, doc }) => { - const indexPatternChange = !_.isEqual( - state.indexPatternTitles, - indexPatternTitles - ); - const docChange = !_.isEqual(state.persistedDoc, doc); - if (indexPatternChange || docChange) { - setState({ - ...state, - indexPatternTitles, - isDirty: docChange, + onChange: ({ filterableIndexPatterns, doc }) => { + lastKnownDocRef.current = doc; + + if (!_.isEqual(state.persistedDoc, doc)) { + setState(s => ({ ...s, isDirty: true })); + } + + // Update the cached index patterns if the user made a change to any of them + if ( + state.indexPatternsForTopNav.length !== filterableIndexPatterns.length || + filterableIndexPatterns.find( + ({ id }) => + !state.indexPatternsForTopNav.find(indexPattern => indexPattern.id === id) + ) + ) { + getAllIndexPatterns( + filterableIndexPatterns, + data.indexPatterns.indexPatterns, + core.notifications + ).then(indexPatterns => { + if (indexPatterns) { + setState(s => ({ ...s, indexPatternsForTopNav: indexPatterns })); + } }); } - lastKnownDocRef.current = doc; }, }} /> @@ -272,3 +308,21 @@ export function App({ ); } + +export async function getAllIndexPatterns( + ids: Array<{ id: string }>, + indexPatternsService: IndexPatternsService, + notifications: NotificationsStart +): Promise { + try { + return await Promise.all(ids.map(({ id }) => indexPatternsService.get(id))); + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.lens.editorFrame.indexPatternLoadingError', { + defaultMessage: 'Error loading index patterns', + }) + ); + + throw new Error(e); + } +} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index 5e81785132616..3b3b12533d74b 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -11,7 +11,8 @@ import chrome from 'ui/chrome'; import { Storage } from 'ui/storage'; import { CoreSetup, CoreStart } from 'src/core/public'; import { npSetup, npStart } from 'ui/new_platform'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public'; +import { start as dataStart } from '../../../../../../src/legacy/core_plugins/data/public/legacy'; import { editorFrameSetup, editorFrameStart, editorFrameStop } from '../editor_frame_plugin'; import { indexPatternDatasourceSetup, indexPatternDatasourceStop } from '../indexpattern_plugin'; import { SavedObjectIndexStore } from '../persistence'; @@ -25,7 +26,7 @@ import { App } from './app'; import { EditorFrameInstance } from '../types'; export interface LensPluginStartDependencies { - data: DataPublicPluginStart; + data: DataStart; } export class AppPlugin { private instance: EditorFrameInstance | null = null; @@ -33,7 +34,7 @@ export class AppPlugin { constructor() {} - setup(core: CoreSetup) { + setup(core: CoreSetup, plugins: {}) { // TODO: These plugins should not be called from the top level, but since this is the // entry point to the app we have no choice until the new platform is ready const indexPattern = indexPatternDatasourceSetup(); @@ -43,10 +44,10 @@ export class AppPlugin { const editorFrameSetupInterface = editorFrameSetup(); this.store = new SavedObjectIndexStore(chrome!.getSavedObjectsClient()); - editorFrameSetupInterface.registerDatasource('indexpattern', indexPattern); editorFrameSetupInterface.registerVisualization(xyVisualization); editorFrameSetupInterface.registerVisualization(datatableVisualization); editorFrameSetupInterface.registerVisualization(metricVisualization); + editorFrameSetupInterface.registerDatasource('indexpattern', indexPattern); } start(core: CoreStart, { data }: LensPluginStartDependencies) { @@ -113,6 +114,6 @@ export class AppPlugin { const app = new AppPlugin(); -export const appSetup = () => app.setup(npSetup.core); -export const appStart = () => app.start(npStart.core, { data: npStart.plugins.data }); +export const appSetup = () => app.setup(npSetup.core, {}); +export const appStart = () => app.start(npStart.core, { data: dataStart }); export const appStop = () => app.stop(); diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx index 177dfc9577028..f649564b2231a 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx @@ -27,6 +27,7 @@ function mockFrame(): FramePublicAPI { fromDate: 'now-7d', toDate: 'now', }, + filters: [], }; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx index 6229e558d1dab..115e8cbf002c3 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx @@ -6,6 +6,7 @@ import React, { useMemo, memo, useContext, useState } from 'react'; import { i18n } from '@kbn/i18n'; +import { Filter } from '@kbn/es-query'; import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; import { Query } from 'src/plugins/data/common'; import { DatasourceDataPanelProps, Datasource } from '../../../public'; @@ -23,6 +24,7 @@ interface DataPanelWrapperProps { core: DatasourceDataPanelProps['core']; query: Query; dateRange: FramePublicAPI['dateRange']; + filters: Filter[]; } export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { @@ -45,6 +47,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { core: props.core, query: props.query, dateRange: props.dateRange, + filters: props.filters, }; const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index 0b4d7ba217532..22766b86a4b15 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactElement } from 'react'; import { ReactWrapper } from 'enzyme'; +import { EuiPanel, EuiToolTip } from '@elastic/eui'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { EditorFrame } from './editor_frame'; import { Visualization, DatasourcePublicAPI, DatasourceSuggestion } from '../../types'; @@ -19,7 +20,7 @@ import { } from '../mocks'; import { ExpressionRenderer } from 'src/legacy/core_plugins/expressions/public'; import { DragDrop } from '../../drag_drop'; -import { EuiPanel, EuiToolTip } from '@elastic/eui'; +import { FrameLayout } from './frame_layout'; // calling this function will wait for all pending Promises from mock // datasources to be processed by its callers. @@ -48,6 +49,7 @@ function getDefaultProps() { onChange: jest.fn(), dateRange: { fromDate: '', toDate: '' }, query: { query: '', language: 'lucene' }, + filters: [], core: coreMock.createSetup(), }; } @@ -256,6 +258,7 @@ describe('editor_frame', () => { addNewLayer: expect.any(Function), removeLayers: expect.any(Function), query: { query: '', language: 'lucene' }, + filters: [], dateRange: { fromDate: 'now-7d', toDate: 'now' }, }); }); @@ -409,56 +412,58 @@ describe('editor_frame', () => { instance.update(); expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` - Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "kibana", - "type": "function", - }, - Object { - "arguments": Object { - "filters": Array [], - "query": Array [ - "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", - ], - "timeRange": Array [ - "{\\"from\\":\\"\\",\\"to\\":\\"\\"}", - ], - }, - "function": "kibana_context", - "type": "function", - }, - Object { - "arguments": Object { - "layerIds": Array [ - "first", - ], - "tables": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource", - "type": "function", - }, - ], - "type": "expression", - }, - ], + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "kibana", + "type": "function", + }, + Object { + "arguments": Object { + "filters": Array [ + "[]", + ], + "query": Array [ + "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", + ], + "timeRange": Array [ + "{\\"from\\":\\"\\",\\"to\\":\\"\\"}", + ], + }, + "function": "kibana_context", + "type": "function", + }, + Object { + "arguments": Object { + "layerIds": Array [ + "first", + ], + "tables": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource", + "type": "function", }, - "function": "lens_merge_tables", - "type": "function", - }, - Object { - "arguments": Object {}, - "function": "vis", - "type": "function", - }, - ], - "type": "expression", - } - `); + ], + "type": "expression", + }, + ], + }, + "function": "lens_merge_tables", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "vis", + "type": "function", + }, + ], + "type": "expression", + } + `); }); it('should render individual expression for each given layer', async () => { @@ -525,7 +530,9 @@ describe('editor_frame', () => { }, Object { "arguments": Object { - "filters": Array [], + "filters": Array [ + "[]", + ], "query": Array [ "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", ], @@ -1491,7 +1498,7 @@ describe('editor_frame', () => { expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenNthCalledWith(1, { - indexPatternTitles: ['resolved'], + filterableIndexPatterns: [{ id: '1', title: 'resolved' }], doc: { expression: '', id: undefined, @@ -1501,6 +1508,7 @@ describe('editor_frame', () => { datasourceStates: { testDatasource: undefined }, query: { query: '', language: 'lucene' }, filters: [], + dateRange: { fromDate: '', toDate: '' }, }, title: 'New visualization', type: 'lens', @@ -1508,7 +1516,7 @@ describe('editor_frame', () => { }, }); expect(onChange).toHaveBeenLastCalledWith({ - indexPatternTitles: ['resolved'], + filterableIndexPatterns: [{ id: '1', title: 'resolved' }], doc: { expression: '', id: undefined, @@ -1520,6 +1528,7 @@ describe('editor_frame', () => { datasourceStates: { testDatasource: undefined }, query: { query: '', language: 'lucene' }, filters: [], + dateRange: { fromDate: '', toDate: '' }, }, title: 'New visualization', type: 'lens', @@ -1567,7 +1576,7 @@ describe('editor_frame', () => { await waitForPromises(); expect(onChange).toHaveBeenCalledTimes(3); expect(onChange).toHaveBeenNthCalledWith(3, { - indexPatternTitles: [], + filterableIndexPatterns: [], doc: { expression: expect.stringContaining('vis "expression"'), id: undefined, @@ -1577,6 +1586,7 @@ describe('editor_frame', () => { visualization: { initialState: true }, query: { query: 'new query', language: 'lucene' }, filters: [], + dateRange: { fromDate: '', toDate: '' }, }, title: 'New visualization', type: 'lens', @@ -1584,5 +1594,44 @@ describe('editor_frame', () => { }, }); }); + + it('should call onChange when the datasource makes an internal state change', async () => { + const onChange = jest.fn(); + + mockDatasource.initialize.mockResolvedValue({}); + mockDatasource.getLayers.mockReturnValue(['first']); + mockDatasource.getMetaData.mockReturnValue({ + filterableIndexPatterns: [{ id: '1', title: 'resolved' }], + }); + mockVisualization.initialize.mockReturnValue({ initialState: true }); + + act(() => { + instance = mount( + + ); + }); + + await waitForPromises(); + expect(onChange).toHaveBeenCalledTimes(2); + + (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: () => ({ + newState: true, + }), + datasourceId: 'testDatasource', + }); + await waitForPromises(); + + expect(onChange).toHaveBeenCalledTimes(3); + }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 054229bde98fb..5d623fa86cd86 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -6,9 +6,16 @@ import React, { useEffect, useReducer } from 'react'; import { CoreSetup, CoreStart } from 'src/core/public'; -import { Query } from '../../../../../../../src/legacy/core_plugins/data/public'; +import { Filter } from '@kbn/es-query'; +import { Query, SavedQuery } from '../../../../../../../src/legacy/core_plugins/data/public'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/expressions/public'; -import { Datasource, DatasourcePublicAPI, FramePublicAPI, Visualization } from '../../types'; +import { + Datasource, + DatasourcePublicAPI, + FramePublicAPI, + Visualization, + DatasourceMetaData, +} from '../../types'; import { reducer, getInitialState } from './state_management'; import { DataPanelWrapper } from './data_panel_wrapper'; import { ConfigPanelWrapper } from './config_panel_wrapper'; @@ -34,7 +41,12 @@ export interface EditorFrameProps { toDate: string; }; query: Query; - onChange: (arg: { indexPatternTitles: string[]; doc: Document }) => void; + filters: Filter[]; + savedQuery?: SavedQuery; + onChange: (arg: { + filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; + doc: Document; + }) => void; } export function EditorFrame(props: EditorFrameProps) { @@ -98,6 +110,7 @@ export function EditorFrame(props: EditorFrameProps) { datasourceLayers, dateRange: props.dateRange, query: props.query, + filters: props.filters, addNewLayer() { const newLayerId = generateId(); @@ -170,7 +183,7 @@ export function EditorFrame(props: EditorFrameProps) { return; } - const indexPatternTitles: string[] = []; + const indexPatterns: DatasourceMetaData['filterableIndexPatterns'] = []; Object.entries(props.datasourceMap) .filter(([id, datasource]) => { const stateWrapper = state.datasourceStates[id]; @@ -181,10 +194,8 @@ export function EditorFrame(props: EditorFrameProps) { ); }) .forEach(([id, datasource]) => { - indexPatternTitles.push( - ...datasource - .getMetaData(state.datasourceStates[id].state) - .filterableIndexPatterns.map(pattern => pattern.title) + indexPatterns.push( + ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns ); }); @@ -201,8 +212,16 @@ export function EditorFrame(props: EditorFrameProps) { framePublicAPI, }); - props.onChange({ indexPatternTitles, doc }); - }, [state.datasourceStates, state.visualization, props.query, props.dateRange, state.title]); + props.onChange({ filterableIndexPatterns: indexPatterns, doc }); + }, [ + state.datasourceStates, + state.visualization, + props.query, + props.dateRange, + props.filters, + props.savedQuery, + state.title, + ]); return ( } configPanel={ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts index da7ddee67453e..1ddfc54cc187b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts @@ -86,7 +86,7 @@ export function prependKibanaContext( arguments: { timeRange: timeRange ? [JSON.stringify(timeRange)] : [], query: query ? [JSON.stringify(query)] : [], - filters: filters ? [JSON.stringify(filters)] : [], + filters: [JSON.stringify(filters || [])], }, }, ...parsedExpression.chain, @@ -121,13 +121,14 @@ export function buildExpression({ const visualizationExpression = visualization.toExpression(visualizationState, framePublicAPI); const expressionContext = removeDateRange - ? { query: framePublicAPI.query } + ? { query: framePublicAPI.query, filters: framePublicAPI.filters } : { query: framePublicAPI.query, timeRange: { from: framePublicAPI.dateRange.fromDate, to: framePublicAPI.dateRange.toDate, }, + filters: framePublicAPI.filters, }; const completeExpression = prependDatasourceExpression( diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts index 6bfe8f70d93c4..b898d33f7a7b1 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { buildExistsFilter } from '@kbn/es-query'; import { getSavedObjectFormat, Props } from './save'; import { createMockDatasource, createMockVisualization } from '../mocks'; @@ -36,6 +37,7 @@ describe('save editor frame state', () => { }, query: { query: '', language: 'lucene' }, dateRange: { fromDate: 'now-7d', toDate: 'now' }, + filters: [buildExistsFilter({ name: '@timestamp' }, { id: 'indexpattern' })], }, }; @@ -83,7 +85,13 @@ describe('save editor frame state', () => { }, visualization: { things: '4_vis_persisted' }, query: { query: '', language: 'lucene' }, - filters: [], + filters: [ + { + meta: { index: 'indexpattern' }, + exists: { field: '@timestamp' }, + }, + ], + dateRange: { fromDate: 'now-7d', toDate: 'now' }, }, title: 'bbb', type: 'lens', diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts index 6c414d9866033..fc567f2d5dab8 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts @@ -58,7 +58,8 @@ export function getSavedObjectFormat({ }, visualization: visualization.getPersistableState(state.visualization.state), query: framePublicAPI.query, - filters: [], // TODO: Support filters + filters: framePublicAPI.filters, + dateRange: framePublicAPI.dateRange, }, }; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts index aa6d7ded87ed9..5168059a33258 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts @@ -26,6 +26,7 @@ describe('editor_frame state management', () => { core: coreMock.createSetup(), dateRange: { fromDate: 'now-7d', toDate: 'now' }, query: { query: '', language: 'lucene' }, + filters: [], }; }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx index 93283534e1186..82ca3d01b73ca 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx @@ -279,7 +279,7 @@ describe('suggestion_panel', () => { expect(passedExpression).toMatchInlineSnapshot(` "kibana - | kibana_context timeRange=\\"{\\\\\\"from\\\\\\":\\\\\\"now-7d\\\\\\",\\\\\\"to\\\\\\":\\\\\\"now\\\\\\"}\\" query=\\"{\\\\\\"query\\\\\\":\\\\\\"\\\\\\",\\\\\\"language\\\\\\":\\\\\\"lucene\\\\\\"}\\" + | kibana_context timeRange=\\"{\\\\\\"from\\\\\\":\\\\\\"now-7d\\\\\\",\\\\\\"to\\\\\\":\\\\\\"now\\\\\\"}\\" query=\\"{\\\\\\"query\\\\\\":\\\\\\"\\\\\\",\\\\\\"language\\\\\\":\\\\\\"lucene\\\\\\"}\\" filters=\\"[]\\" | lens_merge_tables layerIds=\\"first\\" tables={datasource_expression} | test | expression" diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index fc224db743dca..ddb82565e4b8b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -5,6 +5,8 @@ */ import React from 'react'; + +import { buildExistsFilter } from '@kbn/es-query'; import { ExpressionRendererProps } from '../../../../../../../src/legacy/core_plugins/expressions/public'; import { Visualization, FramePublicAPI, TableSuggestion } from '../../types'; import { @@ -153,7 +155,9 @@ describe('workspace_panel', () => { }, Object { "arguments": Object { - "filters": Array [], + "filters": Array [ + "[]", + ], "query": Array [ "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", ], @@ -244,39 +248,39 @@ describe('workspace_panel', () => { expect( (instance.find(expressionRendererMock).prop('expression') as Ast).chain[2].arguments.tables ).toMatchInlineSnapshot(` - Array [ - Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource", - "type": "function", - }, - ], - "type": "expression", - }, - Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource2", - "type": "function", - }, - ], - "type": "expression", - }, - Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource2", - "type": "function", - }, - ], - "type": "expression", - }, - ] - `); + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource", + "type": "function", + }, + ], + "type": "expression", + }, + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource2", + "type": "function", + }, + ], + "type": "expression", + }, + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource2", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); }); it('should run the expression again if the date range changes', async () => { @@ -332,6 +336,62 @@ describe('workspace_panel', () => { expect(expressionRendererMock).toHaveBeenCalledTimes(2); }); + it('should run the expression again if the filters change', async () => { + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + mockDatasource.getLayers.mockReturnValue(['first']); + + mockDatasource.toExpression + .mockReturnValueOnce('datasource') + .mockReturnValueOnce('datasource second'); + + expressionRendererMock = jest.fn(_arg => ); + + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + /> + ); + + // "wait" for the expression to execute + await waitForPromises(); + instance.update(); + + expect(expressionRendererMock).toHaveBeenCalledTimes(1); + + instance.setProps({ + framePublicAPI: { + ...framePublicAPI, + filters: [buildExistsFilter({ name: 'myfield' }, { id: 'index1' })], + }, + }); + + await waitForPromises(); + instance.update(); + + expect(expressionRendererMock).toHaveBeenCalledTimes(2); + }); + describe('expression failures', () => { it('should show an error message if the expression fails to parse', () => { mockDatasource.toExpression.mockReturnValue('|||'); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index 6d0ab402a2971..66fac5d6cf705 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -142,6 +142,7 @@ export function InnerWorkspacePanel({ datasourceStates, framePublicAPI.dateRange, framePublicAPI.query, + framePublicAPI.filters, ]); useEffect(() => { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index 97e1fe8393fc3..f349585ce88a4 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -77,6 +77,7 @@ export function createMockFramePublicAPI(): FrameMock { removeLayers: jest.fn(), dateRange: { fromDate: 'now-7d', toDate: 'now' }, query: { query: '', language: 'lucene' }, + filters: [], }; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx index 7b21ec0cac1c2..f48a8b467e728 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx @@ -59,6 +59,7 @@ describe('editor_frame plugin', () => { onChange: jest.fn(), dateRange: { fromDate: '', toDate: '' }, query: { query: '', language: 'lucene' }, + filters: [], }); instance.unmount(); }).not.toThrowError(); @@ -73,6 +74,7 @@ describe('editor_frame plugin', () => { onChange: jest.fn(), dateRange: { fromDate: '', toDate: '' }, query: { query: '', language: 'lucene' }, + filters: [], }); instance.unmount(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index e27c2e54500cf..cb81ec3d69985 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -76,7 +76,7 @@ export class EditorFramePlugin { const createInstance = (): EditorFrameInstance => { let domElement: Element; return { - mount: (element, { doc, onError, dateRange, query, onChange }) => { + mount: (element, { doc, onError, dateRange, query, filters, savedQuery, onChange }) => { domElement = element; const firstDatasourceId = Object.keys(this.datasources)[0]; const firstVisualizationId = Object.keys(this.visualizations)[0]; @@ -97,6 +97,8 @@ export class EditorFramePlugin { doc={doc} dateRange={dateRange} query={query} + filters={filters} + savedQuery={savedQuery} onChange={onChange} /> , diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx index 9e20db9276ae3..891eb9415d3a4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow, mount } from 'enzyme'; import React, { ChangeEvent } from 'react'; import { IndexPatternPrivateState, IndexPatternColumn } from './indexpattern'; import { createMockedDragDropContext } from './mocks'; @@ -12,6 +11,7 @@ import { InnerIndexPatternDataPanel, IndexPatternDataPanel, MemoizedDataPanel } import { FieldItem } from './field_item'; import { act } from 'react-dom/test-utils'; import { coreMock } from 'src/core/public/mocks'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ChangeIndexPattern } from './change_indexpattern'; jest.mock('ui/new_platform'); @@ -220,6 +220,7 @@ describe('IndexPattern Data Panel', () => { toDate: 'now', }, query: { query: '', language: 'lucene' }, + filters: [], showEmptyFields: false, onToggleEmptyFields: jest.fn(), }; @@ -231,7 +232,7 @@ describe('IndexPattern Data Panel', () => { ...initialState, layers: { first: { indexPatternId: '1', columnOrder: [], columns: {} } }, }; - const wrapper = shallow( + const wrapper = shallowWithIntl( { second: { indexPatternId: '1', columnOrder: [], columns: {} }, }, }; - const wrapper = shallow( + const wrapper = shallowWithIntl( { }, }, }; - const wrapper = shallow( + const wrapper = shallowWithIntl( { }); it('should render a warning if there are no index patterns', () => { - const wrapper = shallow( + const wrapper = shallowWithIntl( ); expect(wrapper.find('[data-test-subj="indexPattern-no-indexpatterns"]')).toHaveLength(1); }); it('should call setState when the index pattern is switched', async () => { - const wrapper = shallow(); + const wrapper = shallowWithIntl(); wrapper.find(ChangeIndexPattern).prop('onChangeIndexPattern')('2'); @@ -333,7 +334,9 @@ describe('IndexPattern Data Panel', () => { }, }); const updateFields = jest.fn(); - mount(); + mountWithIntl( + + ); await waitForPromises(); @@ -400,7 +403,9 @@ describe('IndexPattern Data Panel', () => { const props = { ...defaultProps, indexPatterns: newIndexPatterns }; - mount(); + mountWithIntl( + + ); await waitForPromises(); @@ -410,7 +415,7 @@ describe('IndexPattern Data Panel', () => { describe('while showing empty fields', () => { it('should list all supported fields in the pattern sorted alphabetically', async () => { - const wrapper = shallow( + const wrapper = shallowWithIntl( ); @@ -424,7 +429,7 @@ describe('IndexPattern Data Panel', () => { }); it('should filter down by name', () => { - const wrapper = shallow( + const wrapper = shallowWithIntl( ); @@ -440,7 +445,7 @@ describe('IndexPattern Data Panel', () => { }); it('should filter down by type', () => { - const wrapper = mount( + const wrapper = mountWithIntl( ); @@ -461,7 +466,7 @@ describe('IndexPattern Data Panel', () => { }); it('should toggle type if clicked again', () => { - const wrapper = mount( + const wrapper = mountWithIntl( ); @@ -489,7 +494,7 @@ describe('IndexPattern Data Panel', () => { }); it('should filter down by type and by name', () => { - const wrapper = mount( + const wrapper = mountWithIntl( ); @@ -537,7 +542,7 @@ describe('IndexPattern Data Panel', () => { }); it('should list all supported fields in the pattern sorted alphabetically', async () => { - const wrapper = shallow(); + const wrapper = shallowWithIntl(); expect(wrapper.find(FieldItem).map(fieldItem => fieldItem.prop('field').name)).toEqual([ 'bytes', @@ -546,7 +551,7 @@ describe('IndexPattern Data Panel', () => { }); it('should filter down by name', () => { - const wrapper = shallow( + const wrapper = shallowWithIntl( ); @@ -562,7 +567,7 @@ describe('IndexPattern Data Panel', () => { }); it('should allow removing the filter for data', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); wrapper .find('[data-test-subj="lnsIndexPatternFiltersToggle"]') diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx index da42113b4e7b4..11d6228251025 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx @@ -26,7 +26,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Query } from 'src/plugins/data/common'; import { DatasourceDataPanelProps, DataType } from '../types'; import { IndexPatternPrivateState, IndexPatternField, IndexPattern } from './indexpattern'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; @@ -66,6 +65,7 @@ export function IndexPatternDataPanel({ dragDropContext, core, query, + filters, dateRange, }: DatasourceDataPanelProps) { const { indexPatterns, currentIndexPatternId } = state; @@ -114,6 +114,7 @@ export function IndexPatternDataPanel({ indexPatterns={indexPatterns} query={query} dateRange={dateRange} + filters={filters} dragDropContext={dragDropContext} showEmptyFields={state.showEmptyFields} onToggleEmptyFields={onToggleEmptyFields} @@ -146,18 +147,16 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ indexPatterns, query, dateRange, + filters, dragDropContext, onChangeIndexPattern, updateFieldsWithCounts, showEmptyFields, onToggleEmptyFields, core, -}: Partial & { +}: Pick> & { currentIndexPatternId: string; indexPatterns: Record; - dateRange: DatasourceDataPanelProps['dateRange']; - query: Query; - core: DatasourceDataPanelProps['core']; dragDropContext: DragContextState; showEmptyFields: boolean; onToggleEmptyFields: () => void; @@ -487,6 +486,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ exists={overallField ? !!overallField.exists : false} dateRange={dateRange} query={query} + filters={filters} /> ); })} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx new file mode 100644 index 0000000000000..9956c0ec33061 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx @@ -0,0 +1,215 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { EuiLoadingSpinner, EuiPopover } from '@elastic/eui'; +import { FieldItem, FieldItemProps } from './field_item'; +import { coreMock } from 'src/core/public/mocks'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +jest.mock('ui/new_platform'); + +// Formatter must be mocked to return a string, or the rendering will fail +jest.mock('../../../../../../src/legacy/ui/public/registry/field_formats', () => ({ + fieldFormats: { + getDefaultInstance: jest.fn().mockReturnValue({ + convert: jest.fn().mockReturnValue((s: unknown) => JSON.stringify(s)), + }), + }, +})); + +const waitForPromises = () => new Promise(resolve => setTimeout(resolve)); + +const indexPattern = { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + fields: [ + { + name: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + ], +}; + +describe('IndexPattern Field Item', () => { + let defaultProps: FieldItemProps; + let core: ReturnType; + + beforeEach(() => { + core = coreMock.createSetup(); + core.http.post.mockClear(); + defaultProps = { + indexPattern, + core, + highlight: '', + dateRange: { + fromDate: 'now-7d', + toDate: 'now', + }, + query: { query: '', language: 'lucene' }, + filters: [], + field: { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + exists: true, + }; + }); + + it('should request field stats every time the button is clicked', async () => { + let resolveFunction: (arg: unknown) => void; + + core.http.post.mockImplementation(() => { + return new Promise(resolve => { + resolveFunction = resolve; + }); + }); + + const wrapper = mountWithIntl(); + + wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click'); + + expect(core.http.post).toHaveBeenCalledWith( + `/api/lens/index_stats/my-fake-index-pattern/field`, + { + body: JSON.stringify({ + dslQuery: { + bool: { + must: [{ match_all: {} }], + filter: [], + should: [], + must_not: [], + }, + }, + fromDate: 'now-7d', + toDate: 'now', + timeFieldName: 'timestamp', + field: { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + }), + } + ); + + expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); + + resolveFunction!({ + totalDocuments: 4633, + sampledDocuments: 4633, + sampledValues: 4633, + histogram: { + buckets: [{ count: 705, key: 0 }], + }, + topValues: { + buckets: [{ count: 147, key: 0 }], + }, + }); + + await waitForPromises(); + wrapper.update(); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); + + wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click'); + expect(core.http.post).toHaveBeenCalledTimes(1); + + act(() => { + const closePopover = wrapper.find(EuiPopover).prop('closePopover'); + + closePopover(); + }); + + expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(false); + + act(() => { + wrapper.setProps({ + dateRange: { + fromDate: 'now-14d', + toDate: 'now-7d', + }, + query: { query: 'geo.src : "US"', language: 'kuery' }, + filters: [ + { + match: { phrase: { 'geo.dest': 'US' } }, + }, + ], + }); + }); + + wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click'); + + expect(core.http.post).toHaveBeenCalledTimes(2); + expect(core.http.post).toHaveBeenLastCalledWith( + `/api/lens/index_stats/my-fake-index-pattern/field`, + { + body: JSON.stringify({ + dslQuery: { + bool: { + must: [], + filter: [ + { + bool: { + should: [{ match_phrase: { 'geo.src': 'US' } }], + minimum_should_match: 1, + }, + }, + { + match: { phrase: { 'geo.dest': 'US' } }, + }, + ], + should: [], + must_not: [], + }, + }, + fromDate: 'now-14d', + toDate: 'now-7d', + timeFieldName: 'timestamp', + field: { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + }), + } + ); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx index 62591bdf1e081..af0612be8dc2f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx @@ -34,7 +34,7 @@ import { niceTimeFormatter, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { toElasticsearchQuery } from '@kbn/es-query'; +import { Filter, buildEsQuery, getEsQueryConfig } from '@kbn/es-query'; import { Query } from 'src/plugins/data/common'; // @ts-ignore import { fieldFormats } from '../../../../../../src/legacy/ui/public/registry/field_formats'; @@ -52,6 +52,7 @@ export interface FieldItemProps { exists: boolean; query: Query; dateRange: DatasourceDataPanelProps['dateRange']; + filters: Filter[]; } interface State { @@ -71,7 +72,7 @@ function wrapOnDot(str?: string) { } export function FieldItem(props: FieldItemProps) { - const { core, field, indexPattern, highlight, exists, query, dateRange } = props; + const { core, field, indexPattern, highlight, exists, query, dateRange, filters } = props; const [infoIsOpen, setOpen] = useState(false); @@ -112,7 +113,7 @@ export function FieldItem(props: FieldItemProps) { core.http .post(`/api/lens/index_stats/${indexPattern.title}/field`, { body: JSON.stringify({ - query: toElasticsearchQuery(query, indexPattern), + dslQuery: buildEsQuery(indexPattern, query, filters, getEsQueryConfig(core.uiSettings)), fromDate: dateRange.fromDate, toDate: dateRange.toDate, timeFieldName: indexPattern.timeFieldName, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts index 3576916e81868..98d6ed6f26869 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts @@ -5,10 +5,7 @@ */ import chromeMock from 'ui/chrome'; -import { data as dataMock } from '../../../../../../src/legacy/core_plugins/data/public/setup'; import { Storage } from 'ui/storage'; -import { functionsRegistry } from '../../../../../../src/legacy/core_plugins/interpreter/public/registries'; -import { SavedObjectsClientContract } from 'src/core/public'; import { getIndexPatternDatasource, IndexPatternPersistedState, @@ -25,7 +22,6 @@ jest.mock('../id_generator'); jest.mock('ui/chrome'); // Contains old and new platform data plugins, used for interpreter and filter ratio jest.mock('ui/new_platform'); -jest.mock('plugins/data/setup', () => ({ data: { query: { ui: {} } } })); const expectedIndexPatterns = { 1: { @@ -138,10 +134,7 @@ describe('IndexPattern Data Source', () => { indexPatternDatasource = getIndexPatternDatasource({ chrome: chromeMock, storage: {} as Storage, - interpreter: { functionsRegistry }, - core: coreMock.createSetup(), - data: dataMock, - savedObjectsClient: {} as SavedObjectsClientContract, + core: coreMock.createStart(), }); persistedState = { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index afdbcf5b684c7..f285de4dcbf9d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreSetup, SavedObjectsClientContract } from 'src/core/public'; +import { CoreStart } from 'src/core/public'; import { Storage } from 'ui/storage'; import { i18n } from '@kbn/i18n'; import { @@ -20,7 +20,7 @@ import { import { getIndexPatterns } from './loader'; import { toExpression } from './to_expression'; import { IndexPatternDimensionPanel } from './dimension_panel'; -import { IndexPatternDatasourcePluginPlugins } from './plugin'; +import { IndexPatternDatasourceSetupPlugins } from './plugin'; import { IndexPatternDataPanel } from './datapanel'; import { getDatasourceSuggestionsForField, @@ -182,14 +182,14 @@ function removeProperty(prop: string, object: Record): Record & { + // Core start is being required here because it contains the savedObject client + // In the new platform, this plugin wouldn't be initialized until after setup + core: CoreStart; storage: Storage; - savedObjectsClient: SavedObjectsClientContract; }) { const uiSettings = chrome.getUiSettingsClient(); // Not stateful. State is persisted to the frame @@ -307,7 +307,7 @@ export function getIndexPatternDatasource({ setState={setState} uiSettings={uiSettings} storage={storage} - savedObjectsClient={savedObjectsClient} + savedObjectsClient={core.savedObjects.client} layerId={props.layerId} http={core.http} uniqueLabel={columnLabelMap[props.columnId]} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx index 841d59b602ee8..b7e23b36d55c3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx @@ -5,9 +5,6 @@ */ import chromeMock from 'ui/chrome'; -import { data as dataMock } from '../../../../../../src/legacy/core_plugins/data/public/setup'; -import { functionsRegistry } from '../../../../../../src/legacy/core_plugins/interpreter/public/registries'; -import { SavedObjectsClientContract } from 'src/core/public'; import { getIndexPatternDatasource, IndexPatternPersistedState, @@ -135,12 +132,9 @@ describe('IndexPattern Data Source suggestions', () => { beforeEach(() => { indexPatternDatasource = getIndexPatternDatasource({ - core: coreMock.createSetup(), + core: coreMock.createStart(), chrome: chromeMock, storage: {} as Storage, - interpreter: { functionsRegistry }, - data: dataMock, - savedObjectsClient: {} as SavedObjectsClientContract, }); persistedState = { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx index 581c08f832b67..7e2956cf2bb4b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx @@ -9,22 +9,20 @@ import { CoreSetup } from 'src/core/public'; // The following dependencies on ui/* and src/legacy/core_plugins must be mocked when testing import chrome, { Chrome } from 'ui/chrome'; import { Storage } from 'ui/storage'; -import { npSetup } from 'ui/new_platform'; +import { npSetup, npStart } from 'ui/new_platform'; import { ExpressionFunction } from '../../../../../../src/legacy/core_plugins/interpreter/public'; import { functionsRegistry } from '../../../../../../src/legacy/core_plugins/interpreter/public/registries'; import { getIndexPatternDatasource } from './indexpattern'; import { renameColumns } from './rename_columns'; import { calculateFilterRatio } from './filter_ratio'; -import { setup as dataSetup } from '../../../../../../src/legacy/core_plugins/data/public/legacy'; // TODO these are intermediary types because interpreter is not typed yet // They can get replaced by references to the real interfaces as soon as they // are available -export interface IndexPatternDatasourcePluginPlugins { +export interface IndexPatternDatasourceSetupPlugins { chrome: Chrome; interpreter: InterpreterSetup; - data: typeof dataSetup; } export interface InterpreterSetup { @@ -37,17 +35,9 @@ export interface InterpreterSetup { class IndexPatternDatasourcePlugin { constructor() {} - setup(core: CoreSetup, { interpreter, data }: IndexPatternDatasourcePluginPlugins) { + setup(core: CoreSetup, { interpreter }: IndexPatternDatasourceSetupPlugins) { interpreter.functionsRegistry.register(() => renameColumns); interpreter.functionsRegistry.register(() => calculateFilterRatio); - return getIndexPatternDatasource({ - core, - chrome, - interpreter, - data, - storage: new Storage(localStorage), - savedObjectsClient: chrome.getSavedObjectsClient(), - }); } stop() {} @@ -55,12 +45,18 @@ class IndexPatternDatasourcePlugin { const plugin = new IndexPatternDatasourcePlugin(); -export const indexPatternDatasourceSetup = () => +export const indexPatternDatasourceSetup = () => { plugin.setup(npSetup.core, { chrome, interpreter: { functionsRegistry, }, - data: dataSetup, }); + + return getIndexPatternDatasource({ + core: npStart.core, + chrome, + storage: new Storage(localStorage), + }); +}; export const indexPatternDatasourceStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts index 5fa7e3f0aca4a..77155b2add87a 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts @@ -8,6 +8,7 @@ import { SavedObjectAttributes } from 'src/core/server'; import { Filter } from '@kbn/es-query'; import { Query } from 'src/plugins/data/common'; +import { FramePublicAPI } from '../types'; export interface Document { id?: string; @@ -23,6 +24,7 @@ export interface Document { visualization: unknown; query: Query; filters: Filter[]; + dateRange?: FramePublicAPI['dateRange']; }; } diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 217a694a1861c..71ee2d4c25963 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -5,9 +5,11 @@ */ import { Ast } from '@kbn/interpreter/common'; +import { Filter } from '@kbn/es-query'; import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import { CoreSetup } from 'src/core/public'; import { Query } from 'src/plugins/data/common'; +import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { KibanaDatatable } from '../../../../../src/legacy/core_plugins/interpreter/common'; import { DragContextState } from './drag_drop'; import { Document } from './persistence'; @@ -25,9 +27,14 @@ export interface EditorFrameProps { toDate: string; }; query: Query; + filters: Filter[]; + savedQuery?: SavedQuery; // Frame loader (app or embeddable) is expected to call this when it loads and updates - onChange: (newState: { indexPatternTitles: string[]; doc: Document }) => void; + onChange: (newState: { + filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; + doc: Document; + }) => void; } export interface EditorFrameInstance { mount: (element: Element, props: EditorFrameProps) => void; @@ -165,6 +172,7 @@ export interface DatasourceDataPanelProps { core: Pick; query: Query; dateRange: FramePublicAPI['dateRange']; + filters: Filter[]; } // The only way a visualization has to restrict the query building @@ -278,11 +286,13 @@ export interface VisualizationSuggestion { export interface FramePublicAPI { datasourceLayers: Record; + dateRange: { fromDate: string; toDate: string; }; query: Query; + filters: Filter[]; // Adds a new layer. This has a side effect of updating the datasource state addNewLayer: () => string; diff --git a/x-pack/legacy/plugins/lens/readme.md b/x-pack/legacy/plugins/lens/readme.md index 0ea0778dd17ef..60b4266edadb3 100644 --- a/x-pack/legacy/plugins/lens/readme.md +++ b/x-pack/legacy/plugins/lens/readme.md @@ -7,7 +7,7 @@ Run all tests from the `x-pack` root directory - Unit tests: `node scripts/jest --watch lens` - Functional tests: - Run `node scripts/functional_tests_server` - - Run `node ../scripts/functional_test_runner.js --config ./test/functional/config.js` + - Run `node ../scripts/functional_test_runner.js --config ./test/functional/config.js --grep="lens app"` - You may want to comment out all imports except for Lens in the config file. - API Functional tests: - Run `node scripts/functional_tests_server` diff --git a/x-pack/legacy/plugins/lens/server/routes/field_stats.ts b/x-pack/legacy/plugins/lens/server/routes/field_stats.ts index a57811362c6cf..b1b4cdccc3538 100644 --- a/x-pack/legacy/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/legacy/plugins/lens/server/routes/field_stats.ts @@ -24,7 +24,7 @@ export async function initFieldsRoute(setup: CoreSetup) { }), body: schema.object( { - query: schema.object({}, { allowUnknowns: true }), + dslQuery: schema.object({}, { allowUnknowns: true }), fromDate: schema.string(), toDate: schema.string(), timeFieldName: schema.string(), @@ -43,10 +43,10 @@ export async function initFieldsRoute(setup: CoreSetup) { }, async (context, req, res) => { const requestClient = context.core.elasticsearch.dataClient; - const { fromDate, toDate, timeFieldName, field, query } = req.body; + const { fromDate, toDate, timeFieldName, field, dslQuery } = req.body; try { - const filters = { + const query = { bool: { filter: [ { @@ -57,7 +57,7 @@ export async function initFieldsRoute(setup: CoreSetup) { }, }, }, - query, + dslQuery, ], }, }; @@ -66,7 +66,7 @@ export async function initFieldsRoute(setup: CoreSetup) { requestClient.callAsCurrentUser('search', { index: req.params.indexPatternTitle, body: { - query: filters, + query, aggs, }, // The hits total changed in 7.0 from number to object, unless this flag is set diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap new file mode 100644 index 0000000000000..eec24b23c6e23 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap @@ -0,0 +1,2217 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Flyout apm part one should render normally 1`] = ` + + + +

+ Monitor \`APM\` server with Metricbeat +

+
+
+ + + + + + + + + + + + Close + + + + + Next + + + + +
+`; + +exports[`Flyout apm part two should show instructions to disable internal collection 1`] = ` + + + +

+ + apm-server.yml + , + } + } + /> +

+
+ + + monitoring.enabled: false + + + +

+ +

+
+ , + "title": "Disable self monitoring of the APM server's monitoring metrics", + }, + Object { + "children": +

+ It can take up to 30 seconds to detect data. +

+

+ Last self monitoring was 0 seconds ago. +

+
, + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout apm part two should show instructions to migrate to metricbeat 1`] = ` + + +

+ + + +

+ , + "title": "Install Metricbeat on the same server as the APM server", + }, + Object { + "children": + + metricbeat modules enable beat-xpack + + + +

+ + modules.d/beat-xpack.yml + , + "hosts": + hosts + , + } + } + /> +

+
+ + + + + + + + + , + } + } + /> + + } + /> +
+ , + "title": "Enable and configure the Beat x-pack module in Metricbeat", + }, + Object { + "children": + + + metricbeat.yml + , + } + } + /> + + + + output.elasticsearch: + hosts: ["http://localhost:9200"] ## Monitoring cluster + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" + + + + + + + + + + + , + } + } + /> + + } + /> + + , + "title": "Configure Metricbeat to send to the monitoring cluster", + }, + Object { + "children": +

+ + + +

+
, + "title": "Start Metricbeat", + }, + Object { + "children": , + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout beats part one should render normally 1`] = ` + + + +

+ Monitor \`Beats\` instance with Metricbeat +

+
+
+ + + + + + + + + + + + Close + + + + + Next + + + + +
+`; + +exports[`Flyout beats part two should show instructions to disable internal collection 1`] = ` + + + +

+ + .yml + , + } + } + /> +

+
+ + + monitoring.enabled: false + + + +

+ +

+
+ , + "title": "Disable self monitoring of beat's monitoring metrics", + }, + Object { + "children": +

+ It can take up to 30 seconds to detect data. +

+

+ Last self monitoring was 0 seconds ago. +

+
, + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout beats part two should show instructions to migrate to metricbeat 1`] = ` + + +

+ + + +

+ , + "title": "Install Metricbeat on the same server as this beat", + }, + Object { + "children": + + metricbeat modules enable beat-xpack + + + +

+ + modules.d/beat-xpack.yml + , + "hosts": + hosts + , + } + } + /> +

+
+ + +

+ + + , + } + } + /> +

+ + } + /> + + + + + + + + + , + } + } + /> + + } + /> +
+ , + "title": "Enable and configure the Beat x-pack module in Metricbeat", + }, + Object { + "children": + + + metricbeat.yml + , + } + } + /> + + + + output.elasticsearch: + hosts: ["http://localhost:9200"] ## Monitoring cluster + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" + + + + + + + + + + + , + } + } + /> + + } + /> + + , + "title": "Configure Metricbeat to send to the monitoring cluster", + }, + Object { + "children": +

+ + + +

+
, + "title": "Start Metricbeat", + }, + Object { + "children": , + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout elasticsearch part one should render normally 1`] = ` + + + +

+ Monitor \`Elasticsearch\` node with Metricbeat +

+
+
+ + + + + + + + + + + + Close + + + + + Next + + + + +
+`; + +exports[`Flyout elasticsearch part two should show instructions to disable internal collection 1`] = ` + + + +

+ + xpack.monitoring.elasticsearch.collection.enabled + , + } + } + /> +

+
+ + + PUT _cluster/settings +{ + "persistent": { + "xpack.monitoring.elasticsearch.collection.enabled": false + } +} + + + , + "title": "Disable self monitoring of Elasticsearch monitoring metrics", + }, + Object { + "children": +

+ It can take up to 30 seconds to detect data. +

+

+ Last self monitoring was 0 seconds ago. +

+
, + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout elasticsearch part two should show instructions to migrate to metricbeat 1`] = ` + + +

+ + + +

+ , + "title": "Install Metricbeat on the same server as Elasticsearch", + }, + Object { + "children": + +

+ From the installation directory, run: +

+
+ + + metricbeat modules enable elasticsearch-xpack + + + +

+ + modules.d/elasticsearch-xpack.yml + , + "url": + http://localhost:9200 + , + } + } + /> +

+
+ + + + + + + + + , + } + } + /> + + } + /> +
+ , + "title": "Enable and configure the Elasticsearch x-pack module in Metricbeat", + }, + Object { + "children": + + + metricbeat.yml + , + } + } + /> + + + + output.elasticsearch: + hosts: ["http://localhost:9200"] ## Monitoring cluster + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" + + + + + + + + + + + , + } + } + /> + + } + /> + + , + "title": "Configure Metricbeat to send data to the monitoring cluster", + }, + Object { + "children": +

+ + + +

+
, + "title": "Start Metricbeat", + }, + Object { + "children": , + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout kibana part one should render normally 1`] = ` + + + +

+ Monitor \`Kibana\` instance with Metricbeat +

+
+
+ + + + + + + + + + + + Close + + + + + Next + + + + +
+`; + +exports[`Flyout kibana part two should show instructions to disable internal collection 1`] = ` + + + +

+ + kibana.yml + , + } + } + /> +

+
+ + + xpack.monitoring.kibana.collection.enabled: false + + + +

+ + xpack.monitoring.enabled + , + "defaultValue": + true + , + } + } + /> +

+
+ , + "title": "Disable self monitoring of Kibana monitoring metrics", + }, + Object { + "children": +

+ It can take up to 30 seconds to detect data. +

+

+ Last self monitoring was 0 seconds ago. +

+
, + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout kibana part two should show instructions to migrate to metricbeat 1`] = ` + + +

+ + + +

+ , + "title": "Install Metricbeat on the same server as Kibana", + }, + Object { + "children": + + metricbeat modules enable kibana-xpack + + + +

+ + modules.d/kibana-xpack.yml + , + "hosts": + hosts + , + } + } + /> +

+
+ + + + + + + + + , + } + } + /> + + } + /> +
+ , + "title": "Enable and configure the Kibana x-pack module in Metricbeat", + }, + Object { + "children": + + + metricbeat.yml + , + } + } + /> + + + + output.elasticsearch: + hosts: ["http://localhost:9200"] ## Monitoring cluster + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" + + + + + + + + + + + , + } + } + /> + + } + /> + + , + "title": "Configure Metricbeat to send to the monitoring cluster", + }, + Object { + "children": +

+ + + +

+
, + "title": "Start Metricbeat", + }, + Object { + "children": , + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout logstash part one should render normally 1`] = ` + + + +

+ Monitor \`Logstash\` node with Metricbeat +

+
+
+ + + + + + + + + + + + Close + + + + + Next + + + + +
+`; + +exports[`Flyout logstash part two should show instructions to disable internal collection 1`] = ` + + + +

+ + logstash.yml + , + } + } + /> +

+
+ + + xpack.monitoring.enabled: false + + + +

+ +

+
+ , + "title": "Disable self monitoring of Logstash monitoring metrics", + }, + Object { + "children": +

+ It can take up to 30 seconds to detect data. +

+

+ Last self monitoring was 0 seconds ago. +

+
, + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout logstash part two should show instructions to migrate to metricbeat 1`] = ` + + +

+ + + +

+ , + "title": "Install Metricbeat on the same server as Logstash", + }, + Object { + "children": + + metricbeat modules enable logstash-xpack + + + +

+ + modules.d/logstash-xpack.yml + , + "hosts": + hosts + , + } + } + /> +

+
+ + + + + + + + + , + } + } + /> + + } + /> +
+ , + "title": "Enable and configure the Logstash x-pack module in Metricbeat", + }, + Object { + "children": + + + metricbeat.yml + , + } + } + /> + + + + output.elasticsearch: + hosts: ["http://localhost:9200"] ## Monitoring cluster + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" + + + + + + + + + + + , + } + } + /> + + } + /> + + , + "title": "Configure Metricbeat to send to the monitoring cluster", + }, + Object { + "children": +

+ + + +

+
, + "title": "Start Metricbeat", + }, + Object { + "children": , + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout should render a consistent completion state for all products 1`] = ` +Object { + "children": +

+ Metricbeat is shipping monitoring data. +

+
, + "status": "complete", + "title": "Migration status", +} +`; + +exports[`Flyout should render the beat type for beats for the disabling internal collection step 1`] = ` + + + +

+ + filebeat + .yml + , + } + } + /> +

+
+ + + monitoring.enabled: false + + + +

+ +

+
+ , + "title": "Disable self monitoring of filebeat's monitoring metrics", + }, + Object { + "children": +

+ It can take up to 30 seconds to detect data. +

+

+ Last self monitoring was 0 seconds ago. +

+
, + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout should render the beat type for beats for the enabling metricbeat step 1`] = ` + + +

+ + + +

+ , + "title": "Install Metricbeat on the same server as this filebeat", + }, + Object { + "children": + + metricbeat modules enable beat-xpack + + + +

+ + modules.d/beat-xpack.yml + , + "hosts": + hosts + , + } + } + /> +

+
+ + +

+ + + , + } + } + /> +

+ + } + /> + + + + + + + + + , + } + } + /> + + } + /> +
+ , + "title": "Enable and configure the Beat x-pack module in Metricbeat", + }, + Object { + "children": + + + metricbeat.yml + , + } + } + /> + + + + output.elasticsearch: + hosts: ["http://localhost:9200"] ## Monitoring cluster + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" + + + + + + + + + + + , + } + } + /> + + } + /> + + , + "title": "Configure Metricbeat to send to the monitoring cluster", + }, + Object { + "children": +

+ + + +

+
, + "title": "Start Metricbeat", + }, + Object { + "children": , + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; + +exports[`Flyout should show a restart warning for restarting the primary Kibana 1`] = ` + + + +

+ + kibana.yml + , + } + } + /> +

+
+ + + xpack.monitoring.kibana.collection.enabled: false + + + +

+ + xpack.monitoring.enabled + , + "defaultValue": + true + , + } + } + /> +

+
+ + + + +

+ +

+
+
+
+ , + "title": "Disable self monitoring of Kibana monitoring metrics", + }, + Object { + "children": +

+ It can take up to 30 seconds to detect data. +

+

+ Last self monitoring was 0 seconds ago. +

+
, + "status": "incomplete", + "title": "Migration status", + }, + ] + } + /> +
+`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js new file mode 100644 index 0000000000000..9b4ac21548bb8 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { Flyout } from './flyout'; +import { INSTRUCTION_STEP_ENABLE_METRICBEAT } from '../constants'; +import { + ELASTICSEARCH_SYSTEM_ID, + KIBANA_SYSTEM_ID, + BEATS_SYSTEM_ID, + APM_SYSTEM_ID, + LOGSTASH_SYSTEM_ID +} from '../../../../common/constants'; + +jest.mock('ui/documentation_links', () => ({ + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'current' +})); + +jest.mock('../../../../common', () => ({ + formatTimestampToDuration: () => `0 seconds`, +})); + +const PRODUCTS = [ + { + name: ELASTICSEARCH_SYSTEM_ID + }, + { + name: KIBANA_SYSTEM_ID + }, + { + name: LOGSTASH_SYSTEM_ID + }, + { + name: BEATS_SYSTEM_ID + }, + { + name: APM_SYSTEM_ID + } +]; + +describe('Flyout', () => { + for (const { name } of PRODUCTS) { + describe(`${name}`, () => { + describe('part one', () => { + it('should render normally', () => { + const component = shallow( + {}} + product={{}} + productName={name} + /> + ); + expect(component).toMatchSnapshot(); + }); + }); + + describe('part two', () => { + it('should show instructions to migrate to metricbeat', () => { + const component = shallow( + {}} + product={{ + isInternalCollector: true + }} + productName={name} + /> + ); + component.find('EuiButton').simulate('click'); + component.update(); + expect(component.find('EuiFlyoutBody')).toMatchSnapshot(); + }); + + it('should show instructions to disable internal collection', () => { + const component = shallow( + {}} + product={{ + isPartiallyMigrated: true, + lastInternallyCollectedTimestamp: 0, + }} + meta={{ + secondsAgo: 30 + }} + productName={name} + /> + ); + component.find('EuiButton').simulate('click'); + component.update(); + expect(component.find('EuiFlyoutBody')).toMatchSnapshot(); + }); + }); + }); + } + + it('should render a consistent completion state for all products', () => { + let template = null; + for (const { name } of PRODUCTS) { + const component = shallow( + {}} + product={{ + isPartiallyMigrated: true + }} + meta={{ + secondsAgo: 10 + }} + productName={name} + /> + ); + component.setState({ + activeStep: INSTRUCTION_STEP_ENABLE_METRICBEAT, + }); + component.update(); + const steps = component.find('EuiSteps').prop('steps'); + const step = steps[steps.length - 1]; + if (!template) { + template = step; + expect(template).toMatchSnapshot(); + } else { + expect(template).toEqual(step); + } + } + }); + + it('should render the beat type for beats for the enabling metricbeat step', () => { + const component = shallow( + {}} + product={{ + isInternalCollector: true, + beatType: 'filebeat' + }} + productName={BEATS_SYSTEM_ID} + /> + ); + component.find('EuiButton').simulate('click'); + component.update(); + expect(component.find('EuiFlyoutBody')).toMatchSnapshot(); + }); + + it('should render the beat type for beats for the disabling internal collection step', () => { + const component = shallow( + {}} + product={{ + isPartiallyMigrated: true, + beatType: 'filebeat' + }} + meta={{ + secondsAgo: 30 + }} + productName={BEATS_SYSTEM_ID} + /> + ); + component.find('EuiButton').simulate('click'); + component.update(); + expect(component.find('EuiFlyoutBody')).toMatchSnapshot(); + }); + + it('should show a restart warning for restarting the primary Kibana', () => { + const component = shallow( + {}} + product={{ + isPartiallyMigrated: true, + isPrimary: true + }} + meta={{ + secondsAgo: 30 + }} + productName={KIBANA_SYSTEM_ID} + /> + ); + component.find('EuiButton').simulate('click'); + component.update(); + expect(component.find('EuiFlyoutBody')).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap new file mode 100644 index 0000000000000..12b82be333703 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap @@ -0,0 +1,187 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SetupModeRenderer should render the flyout open 1`] = ` + + + + + + + + + + + , + } + } + /> + + + + + + + + + Exit setup mode + + + + + + + +`; + +exports[`SetupModeRenderer should render with setup mode disabled 1`] = ` + + + +`; + +exports[`SetupModeRenderer should render with setup mode enabled 1`] = ` + + + + + + + + + + , + } + } + /> + + + + + + + + + Exit setup mode + + + + + + + +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js index 079a3e7eeae09..a07a26f64acff 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js @@ -125,7 +125,7 @@ export class SetupModeRenderer extends React.Component { - + diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.test.js b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.test.js new file mode 100644 index 0000000000000..fbcf8db382614 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.test.js @@ -0,0 +1,277 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { shallow } from 'enzyme'; +import { ELASTICSEARCH_SYSTEM_ID } from '../../../common/constants'; + +describe('SetupModeRenderer', () => { + beforeEach(() => jest.resetModules()); + + it('should render with setup mode disabled', () => { + jest.doMock('../../lib/setup_mode', () => ({ + getSetupModeState: () => ({ + enabled: false + }), + initSetupModeState: () => {}, + updateSetupModeData: () => {}, + setSetupModeMenuItem: () => {} + })); + const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + + const ChildComponent = () =>

Hi

; + const scope = {}; + const injector = {}; + const component = shallow( + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render with setup mode enabled', () => { + jest.doMock('../../lib/setup_mode', () => ({ + getSetupModeState: () => ({ + enabled: true, + data: { + elasticsearch: {}, + _meta: {} + } + }), + initSetupModeState: () => {}, + updateSetupModeData: () => {}, + setSetupModeMenuItem: () => {} + })); + const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + + const ChildComponent = () =>

Hi

; + const scope = {}; + const injector = {}; + const component = shallow( + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render the flyout open', () => { + jest.doMock('../../lib/setup_mode', () => ({ + getSetupModeState: () => ({ + enabled: true, + data: { + elasticsearch: { + byUuid: { + + } + }, + _meta: {} + } + }), + initSetupModeState: () => {}, + updateSetupModeData: () => {}, + setSetupModeMenuItem: () => {} + })); + const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + + const ChildComponent = () =>

Hi

; + const scope = {}; + const injector = {}; + const component = shallow( + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> + ); + + component.setState({ isFlyoutOpen: true }); + component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should handle a new node/instance scenario', () => { + jest.doMock('../../lib/setup_mode', () => ({ + getSetupModeState: () => ({ + enabled: true, + data: { + elasticsearch: { + byUuid: {} + }, + _meta: {} + } + }), + initSetupModeState: () => {}, + updateSetupModeData: () => {}, + setSetupModeMenuItem: () => {} + })); + const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + + const ChildComponent = () =>

Hi

; + const scope = {}; + const injector = {}; + const component = shallow( + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> + ); + + component.setState({ isFlyoutOpen: true, instance: null, isSettingUpNew: true }); + component.update(); + expect(component.find('Flyout').prop('product')).toEqual({ isNetNewUser: true }); + }); + + it('should use a new product found in the api response', () => { + const newProduct = { id: 1 }; + + jest.useFakeTimers(); + jest.doMock('../../lib/setup_mode', () => ({ + getSetupModeState: () => ({ + enabled: true, + data: { + elasticsearch: { + byUuid: { + 2: newProduct + } + }, + _meta: {} + } + }), + initSetupModeState: (_scope, _injectir, cb) => { + setTimeout(() => { + cb({ + elasticsearch: { + byUuid: { + 1: {} + } + } + }); + }, 500); + }, + updateSetupModeData: () => {}, + setSetupModeMenuItem: () => {} + })); + const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + + const ChildComponent = () =>

Hi

; + const scope = {}; + const injector = {}; + const component = shallow( + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> + ); + + component.setState({ isFlyoutOpen: true }); + component.update(); + + jest.advanceTimersByTime(1000); + expect(component.state('renderState')).toBe(true); + expect(component.state('newProduct')).toBe(newProduct); + expect(component.find('Flyout').prop('product')).toBe(newProduct); + }); + + it('should set the top menu items', () => { + const newProduct = { id: 1 }; + + const setSetupModeMenuItem = jest.fn(); + jest.doMock('../../lib/setup_mode', () => ({ + getSetupModeState: () => ({ + enabled: true, + data: { + elasticsearch: { + byUuid: { + 2: newProduct + } + }, + _meta: {} + } + }), + initSetupModeState: (_scope, _injectir, cb) => { + setTimeout(() => { + cb({ + elasticsearch: { + byUuid: { + 1: {} + } + } + }); + }, 500); + }, + updateSetupModeData: () => {}, + setSetupModeMenuItem, + })); + const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + + const ChildComponent = () =>

Hi

; + const scope = {}; + const injector = {}; + const component = shallow( + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> + ); + + component.setState({ isFlyoutOpen: true }); + component.update(); + expect(setSetupModeMenuItem).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap new file mode 100644 index 0000000000000..8aba968f8384d --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`setupMode SetupModeBadge internal collection should render each product consistently 1`] = ` + + Monitor with Metricbeat + +`; + +exports[`setupMode SetupModeBadge metricbeat collection should render each product consistently 1`] = ` + + Monitored with Metricbeat + +`; + +exports[`setupMode SetupModeBadge net new user should render each product consistently 1`] = ` + + Monitor with Metricbeat + +`; + +exports[`setupMode SetupModeBadge partially migrated should render each product consistently 1`] = ` + + Disable self monitoring + +`; + +exports[`setupMode SetupModeBadge should use a text status if internal collection cannot be disabled yet for elasticsearch 1`] = ` + + +   + + Some nodes use only self monitoring + + +`; + +exports[`setupMode SetupModeBadge unknown should render each product consistently 1`] = ` + + N/A + +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap new file mode 100644 index 0000000000000..6ec8575a2ab39 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`setupMode formatting formatProductName should format the product name for apm 1`] = `"APM"`; + +exports[`setupMode formatting formatProductName should format the product name for beats 1`] = `"Beats"`; + +exports[`setupMode formatting formatProductName should format the product name for elasticsearch 1`] = `"Elasticsearch"`; + +exports[`setupMode formatting formatProductName should format the product name for kibana 1`] = `"Kibana"`; + +exports[`setupMode formatting formatProductName should format the product name for logstash 1`] = `"Logstash"`; + +exports[`setupMode formatting getIdentifier should get the plural identifier for apm 1`] = `"servers"`; + +exports[`setupMode formatting getIdentifier should get the plural identifier for beats 1`] = `"instances"`; + +exports[`setupMode formatting getIdentifier should get the plural identifier for elasticsearch 1`] = `"nodes"`; + +exports[`setupMode formatting getIdentifier should get the plural identifier for kibana 1`] = `"instances"`; + +exports[`setupMode formatting getIdentifier should get the plural identifier for logstash 1`] = `"nodes"`; + +exports[`setupMode formatting getIdentifier should get the singular identifier for apm 1`] = `"server"`; + +exports[`setupMode formatting getIdentifier should get the singular identifier for beats 1`] = `"instance"`; + +exports[`setupMode formatting getIdentifier should get the singular identifier for elasticsearch 1`] = `"node"`; + +exports[`setupMode formatting getIdentifier should get the singular identifier for kibana 1`] = `"instance"`; + +exports[`setupMode formatting getIdentifier should get the singular identifier for logstash 1`] = `"node"`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap new file mode 100644 index 0000000000000..6f6861a53bf3f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap @@ -0,0 +1,406 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`setupMode ListingCallOut all internally collected should render for apm 1`] = ` + + +

+ These APM servers are self monitored. + Click 'Monitor with Metricbeat' to migrate. +

+
+ +
+`; + +exports[`setupMode ListingCallOut all internally collected should render for beats 1`] = ` + + +

+ These Beats instances are self monitored. + Click 'Monitor with Metricbeat' to migrate. +

+
+ +
+`; + +exports[`setupMode ListingCallOut all internally collected should render for elasticsearch 1`] = ` + + +

+ These Elasticsearch nodes are self monitored. + Click 'Monitor with Metricbeat' to migrate. +

+
+ +
+`; + +exports[`setupMode ListingCallOut all internally collected should render for kibana 1`] = ` + + +

+ These Kibana instances are self monitored. + Click 'Monitor with Metricbeat' to migrate. +

+
+ +
+`; + +exports[`setupMode ListingCallOut all internally collected should render for logstash 1`] = ` + + +

+ These Logstash nodes are self monitored. + Click 'Monitor with Metricbeat' to migrate. +

+
+ +
+`; + +exports[`setupMode ListingCallOut all migrated should render for apm 1`] = ` + + + + +`; + +exports[`setupMode ListingCallOut all migrated should render for beats 1`] = ` + + + + +`; + +exports[`setupMode ListingCallOut all migrated should render for elasticsearch 1`] = ` + + + + +`; + +exports[`setupMode ListingCallOut all migrated should render for kibana 1`] = ` + + + + +`; + +exports[`setupMode ListingCallOut all migrated should render for logstash 1`] = ` + + + + +`; + +exports[`setupMode ListingCallOut all partially migrated should render for apm 1`] = ` + + +

+ Metricbeat is now monitoring your APM servers. Disable self monitoring to finish the migration. +

+
+ +
+`; + +exports[`setupMode ListingCallOut all partially migrated should render for beats 1`] = ` + + +

+ Metricbeat is now monitoring your Beats instances. Disable self monitoring to finish the migration. +

+
+ +
+`; + +exports[`setupMode ListingCallOut all partially migrated should render for elasticsearch 1`] = ` + + +

+ Metricbeat is now monitoring your Elasticsearch nodes. Disable self monitoring to finish the migration. +

+
+ +
+`; + +exports[`setupMode ListingCallOut all partially migrated should render for kibana 1`] = ` + + +

+ Metricbeat is now monitoring your Kibana instances. Disable self monitoring to finish the migration. +

+
+ +
+`; + +exports[`setupMode ListingCallOut all partially migrated should render for logstash 1`] = ` + + +

+ Metricbeat is now monitoring your Logstash nodes. Disable self monitoring to finish the migration. +

+
+ +
+`; + +exports[`setupMode ListingCallOut no detectable instances should render for apm 1`] = ` + + +

+ Click 'Set up monitoring' to start monitoring with Metricbeat. +

+
+ +
+`; + +exports[`setupMode ListingCallOut no detectable instances should render for beats 1`] = ` + + +

+ Click 'Set up monitoring' to start monitoring with Metricbeat. +

+
+ +
+`; + +exports[`setupMode ListingCallOut no detectable instances should render for elasticsearch 1`] = ` + + +

+ Click 'Set up monitoring' to start monitoring with Metricbeat. +

+
+ +
+`; + +exports[`setupMode ListingCallOut no detectable instances should render for kibana 1`] = ` + + +

+ Click 'Set up monitoring' to start monitoring with Metricbeat. +

+
+ +
+`; + +exports[`setupMode ListingCallOut no detectable instances should render for logstash 1`] = ` + + +

+ Click 'Set up monitoring' to start monitoring with Metricbeat. +

+
+ +
+`; + +exports[`setupMode ListingCallOut only detectable instances should render for apm 1`] = ` + + +

+ Click 'Set up monitoring' below to start monitoring this server. +

+
+ +
+`; + +exports[`setupMode ListingCallOut only detectable instances should render for beats 1`] = ` + + +

+ Click 'Set up monitoring' below to start monitoring this instance. +

+
+ +
+`; + +exports[`setupMode ListingCallOut only detectable instances should render for elasticsearch 1`] = ` + + +

+ Click 'Set up monitoring' below to start monitoring this node. +

+
+ +
+`; + +exports[`setupMode ListingCallOut only detectable instances should render for kibana 1`] = ` + + +

+ Click 'Set up monitoring' below to start monitoring this instance. +

+
+ +
+`; + +exports[`setupMode ListingCallOut only detectable instances should render for logstash 1`] = ` + + +

+ Click 'Set up monitoring' below to start monitoring this node. +

+
+ +
+`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap new file mode 100644 index 0000000000000..49ce0e84fdacc --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap @@ -0,0 +1,526 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`setupMode SetupModeTooltip allInternalCollection should render for apm 1`] = ` + + + + Self monitoring + + + +`; + +exports[`setupMode SetupModeTooltip allInternalCollection should render for beats 1`] = ` + + + + Self monitoring + + + +`; + +exports[`setupMode SetupModeTooltip allInternalCollection should render for elasticsearch 1`] = ` + + + + Self monitoring + + + +`; + +exports[`setupMode SetupModeTooltip allInternalCollection should render for kibana 1`] = ` + + + + Self monitoring + + + +`; + +exports[`setupMode SetupModeTooltip allInternalCollection should render for logstash 1`] = ` + + + + Self monitoring + + + +`; + +exports[`setupMode SetupModeTooltip allMonitoredByMetricbeat should render for apm 1`] = ` + + + + Metricbeat monitoring + + + +`; + +exports[`setupMode SetupModeTooltip allMonitoredByMetricbeat should render for beats 1`] = ` + + + + Metricbeat monitoring + + + +`; + +exports[`setupMode SetupModeTooltip allMonitoredByMetricbeat should render for elasticsearch 1`] = ` + + + + Metricbeat monitoring + + + +`; + +exports[`setupMode SetupModeTooltip allMonitoredByMetricbeat should render for kibana 1`] = ` + + + + Metricbeat monitoring + + + +`; + +exports[`setupMode SetupModeTooltip allMonitoredByMetricbeat should render for logstash 1`] = ` + + + + Metricbeat monitoring + + + +`; + +exports[`setupMode SetupModeTooltip internalCollectionOn should render for apm 1`] = ` + + + + Self monitoring is on + + + +`; + +exports[`setupMode SetupModeTooltip internalCollectionOn should render for beats 1`] = ` + + + + Self monitoring is on + + + +`; + +exports[`setupMode SetupModeTooltip internalCollectionOn should render for elasticsearch 1`] = ` + + + + Self monitoring is on + + + +`; + +exports[`setupMode SetupModeTooltip internalCollectionOn should render for kibana 1`] = ` + + + + Self monitoring is on + + + +`; + +exports[`setupMode SetupModeTooltip internalCollectionOn should render for logstash 1`] = ` + + + + Self monitoring is on + + + +`; + +exports[`setupMode SetupModeTooltip no detectable instances should render for apm 1`] = ` + + + + No usage + + + +`; + +exports[`setupMode SetupModeTooltip no detectable instances should render for beats 1`] = ` + + + + No usage + + + +`; + +exports[`setupMode SetupModeTooltip no detectable instances should render for elasticsearch 1`] = ` + + + + No usage + + + +`; + +exports[`setupMode SetupModeTooltip no detectable instances should render for kibana 1`] = ` + + + + No usage + + + +`; + +exports[`setupMode SetupModeTooltip no detectable instances should render for logstash 1`] = ` + + + + No usage + + + +`; + +exports[`setupMode SetupModeTooltip only detectable instances should render for apm 1`] = ` + + + + No monitoring + + + +`; + +exports[`setupMode SetupModeTooltip only detectable instances should render for beats 1`] = ` + + + + No monitoring + + + +`; + +exports[`setupMode SetupModeTooltip only detectable instances should render for elasticsearch 1`] = ` + + + + No monitoring + + + +`; + +exports[`setupMode SetupModeTooltip only detectable instances should render for kibana 1`] = ` + + + + No monitoring + + + +`; + +exports[`setupMode SetupModeTooltip only detectable instances should render for logstash 1`] = ` + + + + No monitoring + + + +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.test.js b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.test.js new file mode 100644 index 0000000000000..55cc9eebb9531 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.test.js @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { SetupModeBadge } from './badge'; +import { + ELASTICSEARCH_SYSTEM_ID, + KIBANA_SYSTEM_ID, + BEATS_SYSTEM_ID, + APM_SYSTEM_ID, + LOGSTASH_SYSTEM_ID +} from '../../../common/constants'; + +const STATUSES = [ + { + name: 'internal collection', + status: { + isInternalCollector: true + } + }, + { + name: 'partially migrated', + status: { + isPartiallyMigrated: true + } + }, + { + name: 'metricbeat collection', + status: { + isFullyMigrated: true + } + }, + { + name: 'net new user', + status: { + isNetNewUser: true + } + }, + { + name: 'unknown', + status: {} + } +]; + +const PRODUCTS = [ + { + name: ELASTICSEARCH_SYSTEM_ID + }, + { + name: KIBANA_SYSTEM_ID + }, + { + name: LOGSTASH_SYSTEM_ID + }, + { + name: BEATS_SYSTEM_ID + }, + { + name: APM_SYSTEM_ID + } +]; + +describe('setupMode SetupModeBadge', () => { + for (const status of STATUSES) { + describe(`${status.name}`, () => { + it('should render each product consistently', () => { + let template = null; + for (const { name } of PRODUCTS) { + const component = shallow( + + ); + if (!template) { + template = component; + expect(component).toMatchSnapshot(); + } else { + expect(template.debug()).toEqual(component.debug()); + } + } + }); + }); + } + + it('should call openFlyout when clicked', () => { + const openFlyout = jest.fn(); + const instance = { + id: 1 + }; + const component = shallow( + + ); + + component.find('EuiBadge').simulate('click'); + expect(openFlyout).toHaveBeenCalledWith(instance); + }); + + it('should use a custom action for the live elasticsearch cluster', () => { + const shortcutToFinishMigration = jest.fn(); + const component = shallow( + + ); + component.find('EuiBadge').simulate('click'); + expect(shortcutToFinishMigration).toHaveBeenCalled(); + }); + + it('should use a text status if internal collection cannot be disabled yet for elasticsearch', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.test.js b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.test.js new file mode 100644 index 0000000000000..06d72bdb0665e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.test.js @@ -0,0 +1,52 @@ +/* + * 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 { formatProductName, getIdentifier } from './formatting'; +import { + ELASTICSEARCH_SYSTEM_ID, + KIBANA_SYSTEM_ID, + BEATS_SYSTEM_ID, + APM_SYSTEM_ID, + LOGSTASH_SYSTEM_ID +} from '../../../common/constants'; + +const PRODUCTS = [ + { + name: ELASTICSEARCH_SYSTEM_ID + }, + { + name: KIBANA_SYSTEM_ID + }, + { + name: LOGSTASH_SYSTEM_ID + }, + { + name: BEATS_SYSTEM_ID + }, + { + name: APM_SYSTEM_ID + } +]; + +describe('setupMode formatting', () => { + describe('formatProductName', () => { + for (const { name } of PRODUCTS) { + it(`should format the product name for ${name}`, () => { + expect(formatProductName(name)).toMatchSnapshot(); + }); + } + }); + describe('getIdentifier', () => { + for (const { name } of PRODUCTS) { + it(`should get the singular identifier for ${name}`, () => { + expect(getIdentifier(name)).toMatchSnapshot(); + }); + it(`should get the plural identifier for ${name}`, () => { + expect(getIdentifier(name, true)).toMatchSnapshot(); + }); + } + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js index adede59d384d6..69f2d07c2d2da 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js @@ -109,8 +109,7 @@ export function ListingCallOut({ setupModeData, productName, customRenderer = nu >

{i18n.translate('xpack.monitoring.setupMode.disableInternalCollectionDescription', { - defaultMessage: `Metricbeat is now monitoring your {product} {identifier}. - Disable self monitoring to finish the migration.`, + defaultMessage: `Metricbeat is now monitoring your {product} {identifier}. Disable self monitoring to finish the migration.`, values: { product: formatProductName(productName), identifier: getIdentifier(productName, true) diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.test.js b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.test.js new file mode 100644 index 0000000000000..5eb223a071965 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.test.js @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { ListingCallOut } from './listing_callout'; +import { + ELASTICSEARCH_SYSTEM_ID, + KIBANA_SYSTEM_ID, + BEATS_SYSTEM_ID, + APM_SYSTEM_ID, + LOGSTASH_SYSTEM_ID +} from '../../../common/constants'; + +const SCENARIOS = [ + { + name: 'no detectable instances', + data: { + totalUniqueInstanceCount: 0, + detected: { + mightExist: false + } + } + }, + { + name: 'only detectable instances', + data: { + totalUniqueInstanceCount: 0, + detected: { + mightExist: true + } + } + }, + { + name: 'all migrated', + data: { + totalUniqueInstanceCount: 1, + totalUniqueFullyMigratedCount: 1 + } + }, + { + name: 'all partially migrated', + data: { + totalUniqueInstanceCount: 1, + totalUniquePartiallyMigratedCount: 1 + } + }, + { + name: 'all internally collected', + data: { + totalUniqueInstanceCount: 1, + totalUniquePartiallyMigratedCount: 0, + totalUniqueFullyMigratedCount: 0 + } + }, +]; + +const PRODUCTS = [ + { + name: ELASTICSEARCH_SYSTEM_ID + }, + { + name: KIBANA_SYSTEM_ID + }, + { + name: LOGSTASH_SYSTEM_ID + }, + { + name: BEATS_SYSTEM_ID + }, + { + name: APM_SYSTEM_ID + } +]; + +describe('setupMode ListingCallOut', () => { + for (const scenario of SCENARIOS) { + describe(`${scenario.name}`, () => { + for (const { name } of PRODUCTS) { + it(`should render for ${name}`, () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + } + }); + } + + it('should render a custom renderer', () => { + const MyComponent =

Hi

; + const component = shallow( + ({ + shouldRender: true, + componentToRender: MyComponent + })} + /> + ); + expect(component.equals(MyComponent)).toBe(true); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.test.js b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.test.js new file mode 100644 index 0000000000000..9c72773ef0d7c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.test.js @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { SetupModeTooltip } from './tooltip'; +import { + ELASTICSEARCH_SYSTEM_ID, + KIBANA_SYSTEM_ID, + BEATS_SYSTEM_ID, + APM_SYSTEM_ID, + LOGSTASH_SYSTEM_ID +} from '../../../common/constants'; + +const SCENARIOS = [ + { + name: 'no detectable instances', + data: { + totalUniqueInstanceCount: 0, + detected: { + mightExist: false + } + } + }, + { + name: 'only detectable instances', + data: { + totalUniqueInstanceCount: 0, + detected: { + mightExist: true + } + } + }, + { + name: 'allMonitoredByMetricbeat', + data: { + totalUniqueInstanceCount: 1, + totalUniqueFullyMigratedCount: 1, + } + }, + { + name: 'internalCollectionOn', + data: { + totalUniqueInstanceCount: 1, + totalUniquePartiallyMigratedCount: 1 + } + }, + { + name: 'allInternalCollection', + data: { + totalUniqueInstanceCount: 1, + totalUniqueFullyMigratedCount: 0, + totalUniquePartiallyMigratedCount: 0, + } + } +]; + +const PRODUCTS = [ + { + name: ELASTICSEARCH_SYSTEM_ID + }, + { + name: KIBANA_SYSTEM_ID + }, + { + name: LOGSTASH_SYSTEM_ID + }, + { + name: BEATS_SYSTEM_ID + }, + { + name: APM_SYSTEM_ID + } +]; + +describe('setupMode SetupModeTooltip', () => { + for (const scenario of SCENARIOS) { + describe(`${scenario.name}`, () => { + for (const { name } of PRODUCTS) { + it(`should render for ${name}`, () => { + const component = shallow( + {}} + /> + ); + expect(component).toMatchSnapshot(); + }); + } + }); + } +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js b/x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js index 53b16c29143bc..862e609cd2fcb 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js +++ b/x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js @@ -50,7 +50,7 @@ export class EuiMonitoringTable extends React.PureComponent { setupMode.openFlyout({}, true)}> {i18n.translate('xpack.monitoring.euiTable.setupNewButtonLabel', { - defaultMessage: 'Set up monitoring for new {identifier}', + defaultMessage: 'Monitor another {identifier} with Metricbeat', values: { identifier: getIdentifier(productName) } diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js new file mode 100644 index 0000000000000..b5878c7ec5181 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js @@ -0,0 +1,196 @@ +/* + * 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 { + toggleSetupMode, + initSetupModeState, + getSetupModeState, + updateSetupModeData, + setSetupModeMenuItem +} from './setup_mode'; + +jest.mock('./ajax_error_handler', () => ({ + ajaxErrorHandlersProvider: err => { + throw err; + } +})); + +let data = {}; + +const injectorModulesMock = { + globalState: { + save: jest.fn() + }, + Private: module => module, + $http: { + post: jest.fn().mockImplementation(() => { + return { data }; + }) + }, + $executor: { + run: jest.fn() + } +}; + +const angularStateMock = { + injector: { + get: module => { + return injectorModulesMock[module] || {}; + } + }, + scope: { + $apply: fn => fn && fn() + } +}; + +// We are no longer waiting for setup mode data to be fetched when enabling +// so we need to wait for the next tick for the async action to finish +function waitForSetupModeData(action) { + process.nextTick(action); +} + +describe('setup_mode', () => { + describe('setup', () => { + afterEach(async () => { + try { + toggleSetupMode(false); + } catch (err) { + // Do nothing... + } + }); + + it('should require angular state', async () => { + let error; + try { + toggleSetupMode(true); + } + catch (err) { + error = err; + } + expect(error).toEqual('Unable to interact with setup ' + + 'mode because the angular injector was not previously set. This needs to be ' + + 'set by calling `initSetupModeState`.'); + }); + + it('should enable toggle mode', async () => { + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + expect(injectorModulesMock.globalState.inSetupMode).toBe(true); + }); + + it('should disable toggle mode', async () => { + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(false); + expect(injectorModulesMock.globalState.inSetupMode).toBe(false); + }); + + it('should set top nav config', async () => { + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + setSetupModeMenuItem(); + expect(angularStateMock.scope.topNavMenu.length).toBe(1); + await toggleSetupMode(true); + expect(angularStateMock.scope.topNavMenu.length).toBe(0); + }); + }); + + describe('in setup mode', () => { + afterEach(async () => { + data = {}; + toggleSetupMode(false); + }); + + it('should enable it through clicking top nav item', async () => { + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await angularStateMock.scope.topNavMenu[0].run(); + expect(injectorModulesMock.globalState.inSetupMode).toBe(true); + }); + + it('should not fetch data if on cloud', async (done) => { + data = { + _meta: { + isOnCloud: true + } + }; + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + waitForSetupModeData(() => { + const state = getSetupModeState(); + expect(state.enabled).toBe(false); + done(); + }); + }); + + it('should set the newly discovered cluster uuid', async (done) => { + const clusterUuid = '1ajy'; + data = { + _meta: { + liveClusterUuid: clusterUuid + }, + elasticsearch: { + byUuid: { + 123: { + isPartiallyMigrated: true + } + } + } + }; + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + waitForSetupModeData(() => { + expect(injectorModulesMock.globalState.cluster_uuid).toBe(clusterUuid); + done(); + }); + }); + + it('should fetch data for a given cluster', async (done) => { + const clusterUuid = '1ajy'; + data = { + _meta: { + liveClusterUuid: clusterUuid + }, + elasticsearch: { + byUuid: { + 123: { + isPartiallyMigrated: true + } + } + } + }; + + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + waitForSetupModeData(() => { + expect(injectorModulesMock.$http.post).toHaveBeenCalledWith( + `../api/monitoring/v1/setup/collection/cluster/${clusterUuid}`, + { ccs: undefined } + ); + done(); + }); + }); + + it('should fetch data for a single node', async () => { + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + injectorModulesMock.$http.post.mockClear(); + await updateSetupModeData('45asd'); + expect(injectorModulesMock.$http.post).toHaveBeenCalledWith( + '../api/monitoring/v1/setup/collection/node/45asd', + { ccs: undefined } + ); + }); + + it('should fetch data without a cluster uuid', async () => { + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + injectorModulesMock.$http.post.mockClear(); + await updateSetupModeData(undefined, true); + expect(injectorModulesMock.$http.post).toHaveBeenCalledWith( + '../api/monitoring/v1/setup/collection/cluster', + { ccs: undefined } + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts index fee17baecce28..da2c184d2d6e0 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts @@ -6,7 +6,6 @@ import { Request } from 'hapi'; -// @ts-ignore no module definition import { buildEsQuery } from '@kbn/es-query'; // @ts-ignore no module definition import { createGenerateCsv } from '../../../csv/server/lib/generate_csv'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/source_status/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/source_status/resolvers.ts index e0597d19a59da..24589822f0250 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/source_status/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/source_status/resolvers.ts @@ -31,9 +31,21 @@ export const createSourceStatusResolvers = (libs: { } => ({ SourceStatus: { async indicesExist(source, args, { req }) { + if ( + args.defaultIndex.length === 1 && + (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all') + ) { + return false; + } return libs.sourceStatus.hasIndices(req, args.defaultIndex); }, async indexFields(source, args, { req }) { + if ( + args.defaultIndex.length === 1 && + (args.defaultIndex[0] === '' || args.defaultIndex[0] === '_all') + ) { + return []; + } return libs.fields.getFields(req, args.defaultIndex); }, }, diff --git a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts b/x-pack/legacy/plugins/uptime/common/constants/plugin.ts index 01f38d37189d1..47f77ea985f32 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/plugin.ts @@ -5,6 +5,7 @@ */ export const PLUGIN = { + APP_ROOT_ID: 'react-uptime-root', ID: 'uptime', ROUTER_BASE_NAME: '/app/uptime#/', LOCAL_STORAGE_KEY: 'xpack.uptime', diff --git a/x-pack/legacy/plugins/uptime/public/app.ts b/x-pack/legacy/plugins/uptime/public/app.ts index 255c51c9e48ce..b068f8a9becda 100644 --- a/x-pack/legacy/plugins/uptime/public/app.ts +++ b/x-pack/legacy/plugins/uptime/public/app.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './apps/kibana_app'; +import './apps/index'; diff --git a/x-pack/legacy/plugins/uptime/public/apps/index.ts b/x-pack/legacy/plugins/uptime/public/apps/index.ts new file mode 100644 index 0000000000000..2fa548c3c2717 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/apps/index.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 chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; +import { Plugin } from './plugin'; + +new Plugin({ opaqueId: Symbol('uptime') }, chrome).start(npStart); diff --git a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts b/x-pack/legacy/plugins/uptime/public/apps/plugin.ts new file mode 100644 index 0000000000000..bc4e30b79cb15 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/apps/plugin.ts @@ -0,0 +1,58 @@ +/* + * 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 { LegacyCoreStart, PluginInitializerContext } from 'src/core/public'; +import { PluginsStart } from 'ui/new_platform/new_platform'; +import { Chrome } from 'ui/chrome'; +import { UMFrontendLibs } from '../lib/lib'; +import { PLUGIN } from '../../common/constants'; +import { getKibanaFrameworkAdapter } from '../lib/adapters/framework/new_platform_adapter'; +import template from './template.html'; +import { UptimeApp } from '../uptime_app'; +import { createApolloClient } from '../lib/adapters/framework/apollo_client_adapter'; + +export interface StartObject { + core: LegacyCoreStart; + plugins: PluginsStart; +} + +export class Plugin { + constructor( + // @ts-ignore this is added to satisfy the New Platform typing constraint, + // but we're not leveraging any of its functionality yet. + private readonly initializerContext: PluginInitializerContext, + private readonly chrome: Chrome + ) { + this.chrome = chrome; + } + + public start(start: StartObject): void { + const { + core, + plugins: { + data: { autocomplete }, + }, + } = start; + const libs: UMFrontendLibs = { + framework: getKibanaFrameworkAdapter(core, autocomplete), + }; + // @ts-ignore improper type description + this.chrome.setRootTemplate(template); + const checkForRoot = () => { + return new Promise(resolve => { + const ready = !!document.getElementById(PLUGIN.APP_ROOT_ID); + if (ready) { + resolve(); + } else { + setTimeout(() => resolve(checkForRoot()), 10); + } + }); + }; + checkForRoot().then(() => { + libs.framework.render(UptimeApp, createApolloClient); + }); + } +} diff --git a/x-pack/legacy/plugins/uptime/public/apps/start_app.tsx b/x-pack/legacy/plugins/uptime/public/apps/start_app.tsx deleted file mode 100644 index 4b07f936ae363..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/apps/start_app.tsx +++ /dev/null @@ -1,19 +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 'react-vis/dist/style.css'; -import 'ui/angular-bootstrap'; -import 'ui/autoload/all'; -import 'ui/autoload/styles'; -import 'ui/courier'; -import 'ui/persisted_log'; -import { createApolloClient } from '../lib/adapters/framework/apollo_client_adapter'; -import { UMFrontendLibs } from '../lib/lib'; -import { UptimeApp } from '../uptime_app'; - -export async function startApp(libs: UMFrontendLibs) { - libs.framework.render(UptimeApp, createApolloClient); -} diff --git a/x-pack/legacy/plugins/uptime/public/apps/template.html b/x-pack/legacy/plugins/uptime/public/apps/template.html new file mode 100644 index 0000000000000..a6fb47048a9b1 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/apps/template.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/x-pack/legacy/plugins/uptime/public/badge.ts b/x-pack/legacy/plugins/uptime/public/badge.ts index 76c2b72f5ead6..a1b4f85348f06 100644 --- a/x-pack/legacy/plugins/uptime/public/badge.ts +++ b/x-pack/legacy/plugins/uptime/public/badge.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Badge } from 'ui/chrome/api/badge'; -export type UMBadge = Badge | undefined; +import { ChromeBadge } from 'src/core/public'; +export type UMBadge = ChromeBadge | undefined; diff --git a/x-pack/legacy/plugins/uptime/public/breadcrumbs.ts b/x-pack/legacy/plugins/uptime/public/breadcrumbs.ts index 31aa1e491a999..ff0dca3887ff2 100644 --- a/x-pack/legacy/plugins/uptime/public/breadcrumbs.ts +++ b/x-pack/legacy/plugins/uptime/public/breadcrumbs.ts @@ -5,24 +5,20 @@ */ import { i18n } from '@kbn/i18n'; +import { ChromeBreadcrumb } from 'src/core/public'; -export interface UMBreadcrumb { - text: string; - href?: string; -} - -const makeOverviewBreadcrumb = (search?: string): UMBreadcrumb => ({ +const makeOverviewBreadcrumb = (search?: string): ChromeBreadcrumb => ({ text: i18n.translate('xpack.uptime.breadcrumbs.overviewBreadcrumbText', { defaultMessage: 'Uptime', }), href: `#/${search ? search : ''}`, }); -export const getOverviewPageBreadcrumbs = (search?: string): UMBreadcrumb[] => [ +export const getOverviewPageBreadcrumbs = (search?: string): ChromeBreadcrumb[] => [ makeOverviewBreadcrumb(search), ]; -export const getMonitorPageBreadcrumb = (name: string, search?: string): UMBreadcrumb[] => [ +export const getMonitorPageBreadcrumb = (name: string, search?: string): ChromeBreadcrumb[] => [ makeOverviewBreadcrumb(search), { text: name }, ]; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx index b979cbf2456bd..f529c9cd9d53f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx @@ -6,18 +6,17 @@ import React, { useState, useEffect, useContext } from 'react'; import { uniqueId, startsWith } from 'lodash'; -import { npStart } from 'ui/new_platform'; import { EuiCallOut } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { StaticIndexPattern } from 'ui/index_patterns'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { AutocompleteProviderRegister, AutocompleteSuggestion } from 'src/plugins/data/public'; +import { StaticIndexPattern } from 'src/legacy/core_plugins/data/public/index_patterns/index_patterns'; import { Typeahead } from './typeahead'; import { getIndexPattern } from '../../../lib/adapters/index_pattern'; import { UptimeSettingsContext } from '../../../contexts'; import { useUrlParams } from '../../../hooks'; import { toStaticIndexPattern } from '../../../lib/helper'; -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; const Container = styled.div` margin-bottom: 10px; @@ -28,10 +27,7 @@ interface State { isLoadingIndexPattern: boolean; } -const getAutocompleteProvider = (language: string) => - npStart.plugins.data.autocomplete.getProvider(language); - -function convertKueryToEsQuery(kuery: string, indexPattern: StaticIndexPattern) { +function convertKueryToEsQuery(kuery: string, indexPattern: unknown) { const ast = fromKueryExpression(kuery); return toElasticsearchQuery(ast, indexPattern); } @@ -39,9 +35,10 @@ function convertKueryToEsQuery(kuery: string, indexPattern: StaticIndexPattern) function getSuggestions( query: string, selectionStart: number, - apmIndexPattern: StaticIndexPattern + apmIndexPattern: StaticIndexPattern, + autocomplete: Pick ) { - const autocompleteProvider = getAutocompleteProvider('kuery'); + const autocompleteProvider = autocomplete.getProvider('kuery'); if (!autocompleteProvider) { return []; } @@ -62,7 +59,11 @@ function getSuggestions( return suggestions; } -export function KueryBar() { +interface Props { + autocomplete: Pick; +} + +export function KueryBar({ autocomplete }: Props) { const [state, setState] = useState({ suggestions: [], isLoadingIndexPattern: true, @@ -94,7 +95,12 @@ export function KueryBar() { currentRequestCheck = currentRequest; try { - let suggestions = await getSuggestions(inputValue, selectionStart, indexPattern); + let suggestions = await getSuggestions( + inputValue, + selectionStart, + indexPattern, + autocomplete + ); suggestions = suggestions .filter((suggestion: AutocompleteSuggestion) => !startsWith(suggestion.text, 'span.')) .slice(0, 15); diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts index 135648d7b6fa3..e3faf34b7696d 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts @@ -4,16 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { capabilities as uiCapabilities } from 'ui/capabilities'; - interface IntegratedAppsAvailability { [key: string]: boolean; } export const getIntegratedAppAvailability = ( + capabilities: any, integratedApps: string[] ): IntegratedAppsAvailability => { - const capabilities = uiCapabilities.get(); return integratedApps.reduce((supportedSolutions: IntegratedAppsAvailability, solutionName) => { supportedSolutions[solutionName] = capabilities[solutionName] && capabilities[solutionName].show === true; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_framework_adapter.ts deleted file mode 100644 index dee8a957e54da..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_framework_adapter.ts +++ /dev/null @@ -1,140 +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 ReactDOM from 'react-dom'; -import { unmountComponentAtNode } from 'react-dom'; -import chrome from 'ui/chrome'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { PLUGIN, INTEGRATED_SOLUTIONS } from '../../../../common/constants'; -import { BootstrapUptimeApp, UMFrameworkAdapter } from '../../lib'; -import { CreateGraphQLClient } from './framework_adapter_types'; -import { renderUptimeKibanaGlobalHelp } from './kibana_global_help'; -import { getTelemetryMonitorPageLogger, getTelemetryOverviewPageLogger } from '../telemetry'; -import { getIntegratedAppAvailability } from './capabilities_adapter'; - -export class UMKibanaFrameworkAdapter implements UMFrameworkAdapter { - private uiRoutes: any; - private xsrfHeader: string; - private uriPath: string; - - constructor(uiRoutes: any) { - this.uiRoutes = uiRoutes; - this.xsrfHeader = chrome.getXsrfToken(); - this.uriPath = `${chrome.getBasePath()}/api/uptime/graphql`; - } - - /** - * This function will acquire all the existing data from Kibana - * services and persisted state expected by the plugin's props - * interface. It then renders the plugin. - */ - public render = ( - renderComponent: BootstrapUptimeApp, - createGraphQLClient: CreateGraphQLClient - ) => { - const route = { - controllerAs: 'uptime', - // @ts-ignore angular - controller: ($scope, $route, config, $location, $window) => { - const graphQLClient = createGraphQLClient(this.uriPath, this.xsrfHeader); - $scope.$$postDigest(() => { - const elem = document.getElementById('uptimeReactRoot'); - - // set up route with current base path - const basePath = chrome.getBasePath(); - const routerBasename = basePath.endsWith('/') - ? `${basePath}/${PLUGIN.ROUTER_BASE_NAME}` - : basePath + PLUGIN.ROUTER_BASE_NAME; - - /** - * TODO: this is a redirect hack to deal with a problem that largely - * in testing but rarely occurs in the real world, where the specified - * URL contains `.../app/uptime{SOME_URL_PARAM_TEXT}#` instead of - * a path like `.../app/uptime#{SOME_URL_PARAM_TEXT}`. - * - * This redirect will almost never be triggered in practice, but it makes more - * sense to include it here rather than altering the existing testing - * infrastructure underlying the rest of Kibana. - * - * We welcome a more permanent solution that will result in the deletion of the - * block below. - */ - if ($location.absUrl().indexOf(PLUGIN.ROUTER_BASE_NAME) === -1) { - $window.location.replace(routerBasename); - } - - // determine whether dark mode is enabled - const darkMode = config.get('theme:darkMode', false) || false; - - /** - * We pass this global help setup as a prop to the app, because for - * localization it's necessary to have the provider mounted before - * we can render our help links, as they rely on i18n. - */ - const renderGlobalHelpControls = () => - // render Uptime feedback link in global help menu - chrome.helpExtension.set((element: HTMLDivElement) => { - ReactDOM.render( - renderUptimeKibanaGlobalHelp(ELASTIC_WEBSITE_URL, DOC_LINK_VERSION), - element - ); - return () => ReactDOM.unmountComponentAtNode(element); - }); - - /** - * These values will let Uptime know if the integrated solutions - * are available. If any/all of them are unavaialble, we should not show - * links/integrations to those apps. - */ - const { - apm: isApmAvailable, - infrastructure: isInfraAvailable, - logs: isLogsAvailable, - } = getIntegratedAppAvailability(INTEGRATED_SOLUTIONS); - - ReactDOM.render( - renderComponent({ - basePath, - client: graphQLClient, - darkMode, - isApmAvailable, - isInfraAvailable, - isLogsAvailable, - logMonitorPageLoad: getTelemetryMonitorPageLogger(this.xsrfHeader, basePath), - logOverviewPageLoad: getTelemetryOverviewPageLogger(this.xsrfHeader, basePath), - renderGlobalHelpControls, - routerBasename, - setBadge: chrome.badge.set, - setBreadcrumbs: chrome.breadcrumbs.set, - }), - elem - ); - this.manageAngularLifecycle($scope, $route, elem); - }); - }, - template: - '', - }; - this.uiRoutes.enable(); - // TODO: hack to refer all routes to same endpoint, use a more proper way of achieving this - this.uiRoutes.otherwise(route); - }; - - // @ts-ignore angular params - private manageAngularLifecycle = ($scope, $route, elem) => { - const lastRoute = $route.current; - const deregister = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - if (lastRoute.$$route && lastRoute.$$route.template === currentRoute.$$route.template) { - $route.current = lastRoute; - } - }); - $scope.$on('$destroy', () => { - deregister(); - unmountComponentAtNode(elem); - }); - }; -} diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx new file mode 100644 index 0000000000000..e35d8e93ca4a7 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ChromeBreadcrumb, CoreStart } from 'src/core/public'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { get } from 'lodash'; +import { AutocompleteProviderRegister } from 'src/plugins/data/public'; +import { CreateGraphQLClient } from './framework_adapter_types'; +import { UptimeApp, UptimeAppProps } from '../../../uptime_app'; +import { getIntegratedAppAvailability } from './capabilities_adapter'; +import { INTEGRATED_SOLUTIONS, PLUGIN } from '../../../../common/constants'; +import { getTelemetryMonitorPageLogger, getTelemetryOverviewPageLogger } from '../telemetry'; +import { renderUptimeKibanaGlobalHelp } from './kibana_global_help'; +import { UMFrameworkAdapter, BootstrapUptimeApp } from '../../lib'; +import { createApolloClient } from './apollo_client_adapter'; + +export const getKibanaFrameworkAdapter = ( + core: CoreStart, + autocomplete: Pick +): UMFrameworkAdapter => { + const { + application: { capabilities }, + chrome: { setBadge, setHelpExtension }, + docLinks: { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL }, + http: { basePath }, + i18n, + } = core; + let breadcrumbs: ChromeBreadcrumb[] = []; + core.chrome.getBreadcrumbs$().subscribe((nextBreadcrumbs?: ChromeBreadcrumb[]) => { + breadcrumbs = nextBreadcrumbs || []; + }); + const { apm, infrastructure, logs } = getIntegratedAppAvailability( + capabilities, + INTEGRATED_SOLUTIONS + ); + const canSave = get(capabilities, 'uptime.save', false); + const props: UptimeAppProps = { + basePath: basePath.get(), + canSave, + client: createApolloClient(`${basePath.get()}/api/uptime/graphql`, 'true'), + darkMode: core.uiSettings.get('theme:darkMode'), + autocomplete, + i18n, + isApmAvailable: apm, + isInfraAvailable: infrastructure, + isLogsAvailable: logs, + kibanaBreadcrumbs: breadcrumbs, + logMonitorPageLoad: getTelemetryMonitorPageLogger('true', basePath.get()), + logOverviewPageLoad: getTelemetryOverviewPageLogger('true', basePath.get()), + renderGlobalHelpControls: () => + setHelpExtension((element: HTMLElement) => { + ReactDOM.render( + renderUptimeKibanaGlobalHelp(ELASTIC_WEBSITE_URL, DOC_LINK_VERSION), + element + ); + return () => ReactDOM.unmountComponentAtNode(element); + }), + routerBasename: basePath.prepend(PLUGIN.ROUTER_BASE_NAME), + setBadge, + setBreadcrumbs: core.chrome.setBreadcrumbs, + }; + + return { + // TODO: these parameters satisfy the interface but are no longer needed + render: async (createComponent: BootstrapUptimeApp, cgc: CreateGraphQLClient) => { + const node = await document.getElementById('react-uptime-root'); + if (node) { + ReactDOM.render(, node); + } + }, + }; +}; diff --git a/x-pack/legacy/plugins/uptime/public/lib/compose/kibana_compose.ts b/x-pack/legacy/plugins/uptime/public/lib/compose/kibana_compose.ts deleted file mode 100644 index 759c161d32cdd..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/compose/kibana_compose.ts +++ /dev/null @@ -1,17 +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 uiRoutes from 'ui/routes'; -import { UMKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; -import { UMFrontendLibs } from '../lib'; - -export function compose(): UMFrontendLibs { - const libs: UMFrontendLibs = { - framework: new UMKibanaFrameworkAdapter(uiRoutes), - }; - - return libs; -} diff --git a/x-pack/legacy/plugins/uptime/public/lib/lib.ts b/x-pack/legacy/plugins/uptime/public/lib/lib.ts index 65a7f18711fd0..0a744bff815c7 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/lib.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/lib.ts @@ -7,8 +7,8 @@ import { NormalizedCacheObject } from 'apollo-cache-inmemory'; import ApolloClient from 'apollo-client'; import React from 'react'; +import { ChromeBreadcrumb } from 'src/core/public'; import { UMBadge } from '../badge'; -import { UMBreadcrumb } from '../breadcrumbs'; import { UptimeAppProps } from '../uptime_app'; import { CreateGraphQLClient } from './adapters/framework/framework_adapter_types'; @@ -16,7 +16,7 @@ export interface UMFrontendLibs { framework: UMFrameworkAdapter; } -export type UMUpdateBreadcrumbs = (breadcrumbs: UMBreadcrumb[]) => void; +export type UMUpdateBreadcrumbs = (breadcrumbs: ChromeBreadcrumb[]) => void; export type UMUpdateBadge = (badge: UMBadge) => void; diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index 3bd09bcba143c..7c1b81b0e15c8 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import React, { Fragment, useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; +import { AutocompleteProviderRegister } from 'src/plugins/data/public'; import { getOverviewPageBreadcrumbs } from '../breadcrumbs'; import { EmptyState, @@ -29,12 +30,13 @@ import { combineFiltersAndUserSearch, stringifyKueries, toStaticIndexPattern } f interface OverviewPageProps { basePath: string; - logOverviewPageLoad: () => void; + autocomplete: Pick; history: any; location: { pathname: string; search: string; }; + logOverviewPageLoad: () => void; setBreadcrumbs: UMUpdateBreadcrumbs; } @@ -54,7 +56,12 @@ const EuiFlexItemStyled = styled(EuiFlexItem)` } `; -export const OverviewPage = ({ basePath, logOverviewPageLoad, setBreadcrumbs }: Props) => { +export const OverviewPage = ({ + basePath, + autocomplete, + logOverviewPageLoad, + setBreadcrumbs, +}: Props) => { const { colors, setHeadingText } = useContext(UptimeSettingsContext); const [getUrlParams, updateUrl] = useUrlParams(); const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams(); @@ -129,7 +136,7 @@ export const OverviewPage = ({ basePath, logOverviewPageLoad, setBreadcrumbs }: - + ; + i18n: I18nStart; isApmAvailable: boolean; isInfraAvailable: boolean; isLogsAvailable: boolean; + kibanaBreadcrumbs: ChromeBreadcrumb[]; logMonitorPageLoad: () => void; logOverviewPageLoad: () => void; routerBasename: string; @@ -50,8 +54,11 @@ export interface UptimeAppProps { const Application = (props: UptimeAppProps) => { const { basePath, + canSave, client, darkMode, + autocomplete, + i18n: i18nCore, isApmAvailable, isInfraAvailable, isLogsAvailable, @@ -89,7 +96,7 @@ const Application = (props: UptimeAppProps) => { useEffect(() => { renderGlobalHelpControls(); setBadge( - !capabilities.get().uptime.save + !canSave ? { text: i18n.translate('xpack.uptime.badge.readOnly.text', { defaultMessage: 'Read only', @@ -140,7 +147,7 @@ const Application = (props: UptimeAppProps) => { }; return ( - + { render={routerProps => ( { /> - + ); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dc42138352783..676cf78de9ff9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5316,11 +5316,8 @@ "xpack.infra.header.logsTitle": "ログ", "xpack.infra.homePage.settingsTabTitle": "設定", "xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage": "{metricId} のモデルには cloudId が必要ですが、{nodeId} に cloudId が指定されていません。", - "xpack.infra.logs.analysis.logRateSectionAnomalySeriesName": "異常", - "xpack.infra.logs.analysis.logRateSectionAreaSeriesName": "期待値", "xpack.infra.logs.analysis.logRateSectionLineSeriesName": "15 分ごとのログエントリー (平均)", "xpack.infra.logs.analysis.logRateSectionLoadingAriaLabel": "ログレートの結果を読み込み中", - "xpack.infra.logs.analysis.logRateSectionModelBoundsCheckboxLabel": "モデルバウンドを表示", "xpack.infra.logs.analysis.logRateSectionNoDataBody": "時間範囲を調整する必要があるかもしれません。", "xpack.infra.logs.analysis.logRateSectionNoDataTitle": "表示するデータがありません。", "xpack.infra.logs.analysis.logRateSectionTitle": "ログレート", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3f647a16d4cb1..c2054497c96f6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5319,11 +5319,8 @@ "xpack.infra.header.logsTitle": "Logs", "xpack.infra.homePage.settingsTabTitle": "设置", "xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage": "{metricId} 的模型需要云 ID,但没有为 {nodeId} 提供。", - "xpack.infra.logs.analysis.logRateSectionAnomalySeriesName": "异常", - "xpack.infra.logs.analysis.logRateSectionAreaSeriesName": "预期", "xpack.infra.logs.analysis.logRateSectionLineSeriesName": "每 15 分钟日志条目数(平均值)", "xpack.infra.logs.analysis.logRateSectionLoadingAriaLabel": "正在加载日志速率结果", - "xpack.infra.logs.analysis.logRateSectionModelBoundsCheckboxLabel": "显示模型边界", "xpack.infra.logs.analysis.logRateSectionNoDataBody": "您可能想调整时间范围。", "xpack.infra.logs.analysis.logRateSectionNoDataTitle": "没有可显示的数据。", "xpack.infra.logs.analysis.logRateSectionTitle": "日志速率", diff --git a/x-pack/test/api_integration/apis/infra/log_analysis.ts b/x-pack/test/api_integration/apis/infra/log_analysis.ts index bd09cdf6ff56e..fe7d55649d1d6 100644 --- a/x-pack/test/api_integration/apis/infra/log_analysis.ts +++ b/x-pack/test/api_integration/apis/infra/log_analysis.ts @@ -20,8 +20,8 @@ import { } from '../../../../legacy/plugins/infra/common/runtime_types'; import { FtrProviderContext } from '../../ftr_provider_context'; -const TIME_BEFORE_START = 1564315100000; -const TIME_AFTER_END = 1565040700000; +const TIME_BEFORE_START = 1569934800000; +const TIME_AFTER_END = 1570016700000; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', }; @@ -32,8 +32,8 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); describe('log analysis apis', () => { - before(() => esArchiver.load('infra/8.0.0/ml_anomalies_log_rate')); - after(() => esArchiver.unload('infra/8.0.0/ml_anomalies_log_rate')); + before(() => esArchiver.load('infra/8.0.0/ml_anomalies_partitioned_log_rate')); + after(() => esArchiver.unload('infra/8.0.0/ml_anomalies_partitioned_log_rate')); describe('log rate results', () => { describe('with the default source', () => { @@ -62,11 +62,12 @@ export default ({ getService }: FtrProviderContext) => { getLogEntryRateSuccessReponsePayloadRT.decode(body), fold(throwErrors(createPlainError), identity) ); - expect(logEntryRateBuckets.data.bucketDuration).to.be(15 * 60 * 1000); expect(logEntryRateBuckets.data.histogramBuckets).to.not.be.empty(); expect( - logEntryRateBuckets.data.histogramBuckets.some(bucket => bucket.anomalies.length > 0) + logEntryRateBuckets.data.histogramBuckets.some(bucket => { + return bucket.dataSets.some(dataSet => dataSet.anomalies.length > 0); + }) ).to.be(true); }); diff --git a/x-pack/test/api_integration/apis/lens/field_stats.ts b/x-pack/test/api_integration/apis/lens/field_stats.ts index 9eba9392c4f7f..b2bb791e2da7f 100644 --- a/x-pack/test/api_integration/apis/lens/field_stats.ts +++ b/x-pack/test/api_integration/apis/lens/field_stats.ts @@ -35,7 +35,7 @@ export default ({ getService }: FtrProviderContext) => { .post('/api/lens/index_stats/logstash/field') .set(COMMON_HEADERS) .send({ - query: { match_all: {} }, + dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -52,7 +52,7 @@ export default ({ getService }: FtrProviderContext) => { .post('/api/lens/index_stats/logstash-2015.09.22/field') .set(COMMON_HEADERS) .send({ - query: { match_all: {} }, + dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -163,7 +163,7 @@ export default ({ getService }: FtrProviderContext) => { .post('/api/lens/index_stats/logstash-2015.09.22/field') .set(COMMON_HEADERS) .send({ - query: { match_all: {} }, + dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -200,7 +200,7 @@ export default ({ getService }: FtrProviderContext) => { .post('/api/lens/index_stats/logstash-2015.09.22/field') .set(COMMON_HEADERS) .send({ - query: { match_all: {} }, + dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -261,6 +261,29 @@ export default ({ getService }: FtrProviderContext) => { }, }); }); + + it('should apply filters and queries', async () => { + const { body } = await supertest + .post('/api/lens/index_stats/logstash-2015.09.22/field') + .set(COMMON_HEADERS) + .send({ + dslQuery: { + bool: { + filter: [{ match: { 'geo.src': 'US' } }], + }, + }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + timeFieldName: '@timestamp', + field: { + name: 'bytes', + type: 'number', + }, + }) + .expect(200); + + expect(body.totalDocuments).to.eql(425); + }); }); }); }; diff --git a/x-pack/test/functional/es_archives/infra/8.0.0/ml_anomalies_partitioned_log_rate/data.json.gz b/x-pack/test/functional/es_archives/infra/8.0.0/ml_anomalies_partitioned_log_rate/data.json.gz new file mode 100644 index 0000000000000..8d15ff8ccb022 Binary files /dev/null and b/x-pack/test/functional/es_archives/infra/8.0.0/ml_anomalies_partitioned_log_rate/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/infra/8.0.0/ml_anomalies_partitioned_log_rate/mappings.json b/x-pack/test/functional/es_archives/infra/8.0.0/ml_anomalies_partitioned_log_rate/mappings.json new file mode 100644 index 0000000000000..69ffc922ede7d --- /dev/null +++ b/x-pack/test/functional/es_archives/infra/8.0.0/ml_anomalies_partitioned_log_rate/mappings.json @@ -0,0 +1,513 @@ +{ + "type": "index", + "value": { + "aliases": { + ".ml-anomalies-.write-kibana-logs-ui-default-default-log-entry-rate": { + }, + ".ml-anomalies-kibana-logs-ui-default-default-log-entry-rate": { + "filter": { + "term": { + "job_id": { + "boost": 1, + "value": "kibana-logs-ui-default-default-log-entry-rate" + } + } + } + } + }, + "index": ".ml-anomalies-shared", + "mappings": { + "_meta": { + "version": "8.0.0" + }, + "dynamic_templates": [ + { + "strings_as_keywords": { + "mapping": { + "type": "keyword" + }, + "match": "*" + } + } + ], + "properties": { + "actual": { + "type": "double" + }, + "all_field_values": { + "analyzer": "whitespace", + "type": "text" + }, + "anomaly_score": { + "type": "double" + }, + "average_bucket_processing_time_ms": { + "type": "double" + }, + "bucket_allocation_failures_count": { + "type": "long" + }, + "bucket_count": { + "type": "long" + }, + "bucket_influencers": { + "properties": { + "anomaly_score": { + "type": "double" + }, + "bucket_span": { + "type": "long" + }, + "influencer_field_name": { + "type": "keyword" + }, + "initial_anomaly_score": { + "type": "double" + }, + "is_interim": { + "type": "boolean" + }, + "job_id": { + "type": "keyword" + }, + "probability": { + "type": "double" + }, + "raw_anomaly_score": { + "type": "double" + }, + "result_type": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + } + }, + "type": "nested" + }, + "bucket_span": { + "type": "long" + }, + "by_field_name": { + "type": "keyword" + }, + "by_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "category_id": { + "type": "long" + }, + "causes": { + "properties": { + "actual": { + "type": "double" + }, + "by_field_name": { + "type": "keyword" + }, + "by_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "correlated_by_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "field_name": { + "type": "keyword" + }, + "function": { + "type": "keyword" + }, + "function_description": { + "type": "keyword" + }, + "over_field_name": { + "type": "keyword" + }, + "over_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "partition_field_name": { + "type": "keyword" + }, + "partition_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "probability": { + "type": "double" + }, + "typical": { + "type": "double" + } + }, + "type": "nested" + }, + "description": { + "type": "text" + }, + "detector_index": { + "type": "integer" + }, + "earliest_record_timestamp": { + "type": "date" + }, + "empty_bucket_count": { + "type": "long" + }, + "event": { + "properties": { + "dataset": { + "type": "keyword" + } + } + }, + "event_count": { + "type": "long" + }, + "examples": { + "type": "text" + }, + "exponential_average_bucket_processing_time_ms": { + "type": "double" + }, + "exponential_average_calculation_context": { + "properties": { + "incremental_metric_value_ms": { + "type": "double" + }, + "latest_timestamp": { + "type": "date" + }, + "previous_exponential_average_ms": { + "type": "double" + } + } + }, + "field_name": { + "type": "keyword" + }, + "forecast_create_timestamp": { + "type": "date" + }, + "forecast_end_timestamp": { + "type": "date" + }, + "forecast_expiry_timestamp": { + "type": "date" + }, + "forecast_id": { + "type": "keyword" + }, + "forecast_lower": { + "type": "double" + }, + "forecast_memory_bytes": { + "type": "long" + }, + "forecast_messages": { + "type": "keyword" + }, + "forecast_prediction": { + "type": "double" + }, + "forecast_progress": { + "type": "double" + }, + "forecast_start_timestamp": { + "type": "date" + }, + "forecast_status": { + "type": "keyword" + }, + "forecast_upper": { + "type": "double" + }, + "function": { + "type": "keyword" + }, + "function_description": { + "type": "keyword" + }, + "influencer_field_name": { + "type": "keyword" + }, + "influencer_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "influencer_score": { + "type": "double" + }, + "influencers": { + "properties": { + "influencer_field_name": { + "type": "keyword" + }, + "influencer_field_values": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + } + }, + "type": "nested" + }, + "initial_anomaly_score": { + "type": "double" + }, + "initial_influencer_score": { + "type": "double" + }, + "initial_record_score": { + "type": "double" + }, + "input_bytes": { + "type": "long" + }, + "input_field_count": { + "type": "long" + }, + "input_record_count": { + "type": "long" + }, + "invalid_date_count": { + "type": "long" + }, + "is_interim": { + "type": "boolean" + }, + "job_id": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "last_data_time": { + "type": "date" + }, + "latest_empty_bucket_timestamp": { + "type": "date" + }, + "latest_record_time_stamp": { + "type": "date" + }, + "latest_record_timestamp": { + "type": "date" + }, + "latest_result_time_stamp": { + "type": "date" + }, + "latest_sparse_bucket_timestamp": { + "type": "date" + }, + "log_time": { + "type": "date" + }, + "max_matching_length": { + "type": "long" + }, + "maximum_bucket_processing_time_ms": { + "type": "double" + }, + "memory_status": { + "type": "keyword" + }, + "min_version": { + "type": "keyword" + }, + "minimum_bucket_processing_time_ms": { + "type": "double" + }, + "missing_field_count": { + "type": "long" + }, + "model_bytes": { + "type": "long" + }, + "model_bytes_exceeded": { + "type": "keyword" + }, + "model_bytes_memory_limit": { + "type": "keyword" + }, + "model_feature": { + "type": "keyword" + }, + "model_lower": { + "type": "double" + }, + "model_median": { + "type": "double" + }, + "model_size_stats": { + "properties": { + "bucket_allocation_failures_count": { + "type": "long" + }, + "job_id": { + "type": "keyword" + }, + "log_time": { + "type": "date" + }, + "memory_status": { + "type": "keyword" + }, + "model_bytes": { + "type": "long" + }, + "model_bytes_exceeded": { + "type": "keyword" + }, + "model_bytes_memory_limit": { + "type": "keyword" + }, + "result_type": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "total_by_field_count": { + "type": "long" + }, + "total_over_field_count": { + "type": "long" + }, + "total_partition_field_count": { + "type": "long" + } + } + }, + "model_upper": { + "type": "double" + }, + "multi_bucket_impact": { + "type": "double" + }, + "out_of_order_timestamp_count": { + "type": "long" + }, + "over_field_name": { + "type": "keyword" + }, + "over_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "partition_field_name": { + "type": "keyword" + }, + "partition_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "probability": { + "type": "double" + }, + "processed_field_count": { + "type": "long" + }, + "processed_record_count": { + "type": "long" + }, + "processing_time_ms": { + "type": "long" + }, + "quantiles": { + "enabled": false, + "type": "object" + }, + "raw_anomaly_score": { + "type": "double" + }, + "record_score": { + "type": "double" + }, + "regex": { + "type": "keyword" + }, + "result_type": { + "type": "keyword" + }, + "retain": { + "type": "boolean" + }, + "scheduled_events": { + "type": "keyword" + }, + "search_count": { + "type": "long" + }, + "snapshot_doc_count": { + "type": "integer" + }, + "snapshot_id": { + "type": "keyword" + }, + "sparse_bucket_count": { + "type": "long" + }, + "terms": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "total_by_field_count": { + "type": "long" + }, + "total_over_field_count": { + "type": "long" + }, + "total_partition_field_count": { + "type": "long" + }, + "total_search_time_ms": { + "type": "double" + }, + "typical": { + "type": "double" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "query": { + "default_field": "all_field_values" + }, + "translog": { + "durability": "async" + }, + "unassigned": { + "node_left": { + "delayed_timeout": "1m" + } + } + } + } + } +} \ No newline at end of file