diff --git a/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx b/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx index 329d1a2455a6f..a059bbd286112 100644 --- a/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx +++ b/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx @@ -10,7 +10,7 @@ import { Route } from 'react-router-dom'; import euiStyled from '../../../../../common/eui_styled_components'; interface TabConfiguration { - title: string; + title: string | React.ReactNode; path: string; } diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts index 294a78cc85206..418b1e7439633 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts @@ -7,3 +7,4 @@ export * from './log_analysis_capabilities'; export * from './log_analysis_jobs'; export * from './log_analysis_results'; +export * from './log_analysis_results_url_state'; 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 new file mode 100644 index 0000000000000..f54402a1a8707 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_graph_data/log_entry_rate.tsx @@ -0,0 +1,71 @@ +/* + * 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_analysis_results.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx index 861c07c3ad5fa..1ab7392b3a76d 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx @@ -5,15 +5,34 @@ */ import createContainer from 'constate-latest'; -import { useMemo } from 'react'; +import { useMemo, useEffect } from 'react'; import { useLogEntryRate } from './log_entry_rate'; -export const useLogAnalysisResults = ({ sourceId }: { sourceId: string }) => { - const { isLoading: isLoadingLogEntryRate, logEntryRate } = useLogEntryRate({ sourceId }); +export const useLogAnalysisResults = ({ + sourceId, + startTime, + endTime, + bucketDuration = 15 * 60 * 1000, +}: { + sourceId: string; + startTime: number; + endTime: number; + bucketDuration?: number; +}) => { + const { isLoading: isLoadingLogEntryRate, logEntryRate, getLogEntryRate } = useLogEntryRate({ + sourceId, + startTime, + endTime, + bucketDuration, + }); const isLoading = useMemo(() => isLoadingLogEntryRate, [isLoadingLogEntryRate]); + useEffect(() => { + getLogEntryRate(); + }, [sourceId, startTime, endTime, bucketDuration]); + return { isLoading, logEntryRate, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx new file mode 100644 index 0000000000000..8ae644a497e19 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { useEffect } from 'react'; +import * as rt from 'io-ts'; +import { useUrlState } from '../../../utils/use_url_state'; +import { timeRangeRT } from '../../../../common/http_api/shared/time_range'; + +const autoRefreshRT = rt.union([rt.boolean, rt.undefined]); +const urlTimeRangeRT = rt.union([timeRangeRT, rt.undefined]); + +const TIME_RANGE_URL_STATE_KEY = 'timeRange'; +const AUTOREFRESH_URL_STATE_KEY = 'autoRefresh'; + +export const useLogAnalysisResultsUrlState = () => { + const [timeRange, setTimeRange] = useUrlState({ + defaultState: { + startTime: moment + .utc() + .subtract(2, 'weeks') + .valueOf(), + endTime: moment.utc().valueOf(), + }, + decodeUrlState: (value: unknown) => urlTimeRangeRT.decode(value).getOrElse(undefined), + encodeUrlState: urlTimeRangeRT.encode, + urlStateKey: TIME_RANGE_URL_STATE_KEY, + }); + + useEffect(() => { + setTimeRange(timeRange); + }, []); + + const [autoRefreshEnabled, setAutoRefresh] = useUrlState({ + defaultState: false, + decodeUrlState: (value: unknown) => autoRefreshRT.decode(value).getOrElse(undefined), + encodeUrlState: autoRefreshRT.encode, + urlStateKey: AUTOREFRESH_URL_STATE_KEY, + }); + + useEffect(() => { + setAutoRefresh(autoRefreshEnabled); + }, []); + + return { + timeRange, + setTimeRange, + autoRefreshEnabled, + setAutoRefresh, + }; +}; 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 aee953f6b0f3b..18e73c540c115 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 @@ -18,7 +18,17 @@ import { useTrackedPromise } from '../../../utils/use_tracked_promise'; type LogEntryRateResults = GetLogEntryRateSuccessResponsePayload['data']; -export const useLogEntryRate = ({ sourceId }: { sourceId: string }) => { +export const useLogEntryRate = ({ + sourceId, + startTime, + endTime, + bucketDuration, +}: { + sourceId: string; + startTime: number; + endTime: number; + bucketDuration: number; +}) => { const [logEntryRate, setLogEntryRate] = useState(null); const [getLogEntryRateRequest, getLogEntryRate] = useTrackedPromise( @@ -31,12 +41,12 @@ export const useLogEntryRate = ({ sourceId }: { sourceId: string }) => { body: JSON.stringify( getLogEntryRateRequestPayloadRT.encode({ data: { - sourceId, // TODO: get from hook arguments + sourceId, timeRange: { - startTime: Date.now(), // TODO: get from hook arguments - endTime: Date.now() + 1000 * 60 * 60, // TODO: get from hook arguments + startTime, + endTime, }, - bucketDuration: 15 * 60 * 1000, // TODO: get from hook arguments + bucketDuration, }, }) ), @@ -50,7 +60,7 @@ export const useLogEntryRate = ({ sourceId }: { sourceId: string }) => { setLogEntryRate(data); }, }, - [sourceId] + [sourceId, startTime, endTime, bucketDuration] ); const isLoading = useMemo(() => getLogEntryRateRequest.state === 'pending', [ 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 new file mode 100644 index 0000000000000..df0eca449bb9f --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/chart_helpers/index.tsx @@ -0,0 +1,20 @@ +/* + * 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.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.tsx index ef4a1ffe6735c..bc0dbfcae94a4 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.tsx @@ -36,7 +36,7 @@ export const AnalysisPage = () => { ) : isSetupRequired ? ( ) : ( - + )} 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 a570f9e37309f..de8c4afda6b1d 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 @@ -4,13 +4,189 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiSuperDatePicker, + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPanel, + EuiBadge, +} from '@elastic/eui'; +import dateMath from '@elastic/datemath'; +import moment from 'moment'; import { useTrackPageview } from '../../../hooks/use_track_metric'; +import { useInterval } from '../../../hooks/use_interval'; +import { useLogAnalysisResults } from '../../../containers/logs/log_analysis'; +import { useLogAnalysisResultsUrlState } from '../../../containers/logs/log_analysis'; +import { LoadingPage } from '../../../components/loading_page'; +import { LogRateResults } from './sections/log_rate'; -export const AnalysisResultsContent = () => { +const DATE_PICKER_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; + +const getLoadingState = () => { + return ( + + ); +}; + +export const AnalysisResultsContent = ({ sourceId }: { sourceId: string }) => { useTrackPageview({ app: 'infra_logs', path: 'analysis_results' }); useTrackPageview({ app: 'infra_logs', path: 'analysis_results', delay: 15000 }); - return
Results
; + const { + timeRange, + setTimeRange, + autoRefreshEnabled, + setAutoRefresh, + } = useLogAnalysisResultsUrlState(); + + const [refreshInterval, setRefreshInterval] = useState(300000); + + const setTimeRangeToNow = useCallback(() => { + const range = timeRange.endTime - timeRange.startTime; + const nowInMs = moment() + .utc() + .valueOf(); + setTimeRange({ + startTime: nowInMs - range, + endTime: nowInMs, + }); + }, [timeRange.startTime, timeRange.endTime, setTimeRange]); + + useInterval(setTimeRangeToNow, autoRefreshEnabled ? refreshInterval : null); + + const bucketDuration = useMemo(() => { + // This function takes the current time range in ms, + // works out the bucket interval we'd need to always + // display 200 data points, and then takes that new + // value and works out the nearest multiple of + // 900000 (15 minutes) to it, so that we don't end up with + // jaggy bucket boundaries between the ML buckets and our + // aggregation buckets. + const msRange = timeRange.endTime - timeRange.startTime; + const bucketIntervalInMs = msRange / 200; + const bucketSpan = 900000; // TODO: Pull this from 'common' when setup hook PR is merged + const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan); + const roundedResult = parseInt(Number(result).toFixed(0), 10); + return roundedResult < bucketSpan ? bucketSpan : roundedResult; + }, [timeRange]); + const { isLoading, logEntryRate } = useLogAnalysisResults({ + sourceId, + startTime: timeRange.startTime, + endTime: timeRange.endTime, + bucketDuration, + }); + const handleTimeRangeChange = useCallback( + ({ start, end }: { start: string; end: string }) => { + const parsedStart = dateMath.parse(start); + const parsedEnd = dateMath.parse(end); + setTimeRange({ + startTime: + !parsedStart || !parsedStart.isValid() ? timeRange.startTime : parsedStart.valueOf(), + endTime: !parsedEnd || !parsedEnd.isValid() ? timeRange.endTime : parsedEnd.valueOf(), + }); + }, + [setTimeRange, timeRange] + ); + + const anomaliesDetected = useMemo(() => { + if (!logEntryRate) { + return null; + } else { + if (logEntryRate.histogramBuckets && logEntryRate.histogramBuckets.length) { + return logEntryRate.histogramBuckets.reduce((acc: any, bucket) => { + if (bucket.anomalies.length > 0) { + return ( + acc + + bucket.anomalies.reduce((anomalyAcc: any, anomaly) => { + return anomalyAcc + 1; + }, 0) + ); + } else { + return acc; + } + }, 0); + } else { + return null; + } + } + }, [logEntryRate]); + + return ( + <> + {isLoading && !logEntryRate ? ( + <>{getLoadingState()} + ) : ( + <> + + + + + + + {anomaliesDetected !== null ? ( + <> + + 0 + ) : ( + {anomaliesDetected} + ), + }} + /> + + + ) : null} + + + + + { + if (isPaused) { + setAutoRefresh(false); + } else { + setRefreshInterval(interval); + setAutoRefresh(true); + } + }} + /> + + + + + + + + + + + + + + + )} + + ); }; 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 new file mode 100644 index 0000000000000..e12a4d8513af2 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/chart.tsx @@ -0,0 +1,179 @@ +/* + * 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 { first, last } from 'lodash'; +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'; + +const areaSeriesColour = 'rgb(224, 237, 255)'; +const lineSeriesColour = 'rgb(49, 133, 252)'; + +interface Props { + data: GetLogEntryRateSuccessResponsePayload['data'] | null; +} + +export const ChartView = ({ data }: Props) => { + const showModelBoundsLabel = i18n.translate( + 'xpack.infra.logs.analysis.logRateSectionModelBoundsCheckboxLabel', + { defaultMessage: 'Show model bounds' } + ); + + const { areaSeries, lineSeries, anomalySeries } = useLogEntryRateGraphData({ data }); + + const dateFormatter = useMemo( + () => + lineSeries.length > 0 + ? niceTimeFormatter([first(lineSeries)[0], last(lineSeries)[0]]) + : (value: number) => `${value}`, + [lineSeries] + ); + + const areaSpecId = getSpecId('modelBounds'); + const lineSpecId = getSpecId('averageValues'); + const anomalySpecId = getSpecId('anomalies'); + + const [dateFormat] = useKibanaUiSetting('dateFormat'); + + const tooltipProps = { + headerFormatter: useCallback( + (tooltipData: TooltipValue) => + moment(tooltipData.value).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'), + [dateFormat] + ), + }; + + const [isShowingModelBounds, setIsShowingModelBounds] = useState(true); + + return ( + <> + + + { + setIsShowingModelBounds(e.target.checked); + }} + /> + + +
+ + + Number(value).toFixed(0)} + /> + {isShowingModelBounds ? ( + + ) : null} + + + + +
+ + ); +}; 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 new file mode 100644 index 0000000000000..e98050f7e83e9 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/index.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingChart, + EuiSpacer, + EuiEmptyPrompt, +} from '@elastic/eui'; +import { GetLogEntryRateSuccessResponsePayload } from '../../../../../../common/http_api/log_analysis/results/log_entry_rate'; +import { ViewSwitcher } from './view_switcher'; +import { ChartView } from './chart'; +import { TableView } from './table'; + +export enum ViewMode { + chart = 'chart', + table = 'table', +} + +export const LogRateResults = ({ + isLoading, + results, +}: { + isLoading: boolean; + results: GetLogEntryRateSuccessResponsePayload['data'] | null; +}) => { + const title = i18n.translate('xpack.infra.logs.analysis.logRateSectionTitle', { + defaultMessage: 'Log entry anomalies', + }); + + const loadingAriaLabel = i18n.translate( + 'xpack.infra.logs.analysis.logRateSectionLoadingAriaLabel', + { defaultMessage: 'Loading log rate results' } + ); + + const [viewMode, setViewMode] = useState(ViewMode.chart); + + return ( + <> + +

{title}

+
+ + {isLoading ? ( + + + + + + ) : !results || (results && results.histogramBuckets && !results.histogramBuckets.length) ? ( + + {i18n.translate('xpack.infra.logs.analysis.logRateSectionNoDataTitle', { + defaultMessage: 'There is no data to display.', + })} + + } + titleSize="m" + body={ +

+ {i18n.translate('xpack.infra.logs.analysis.logRateSectionNoDataBody', { + defaultMessage: 'Try adjusting your time range', + })} +

+ } + /> + ) : ( + <> + + + setViewMode(id as ViewMode)} /> + + + + {viewMode === ViewMode.chart ? ( + + ) : ( + + )} + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/table.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/table.tsx new file mode 100644 index 0000000000000..19bfaacf5d5df --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/table.tsx @@ -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, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { EuiInMemoryTable } from '@elastic/eui'; +import { GetLogEntryRateSuccessResponsePayload } from '../../../../../../common/http_api/log_analysis/results/log_entry_rate'; +import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting'; + +interface Props { + data: GetLogEntryRateSuccessResponsePayload['data']; +} + +const startTimeLabel = i18n.translate( + 'xpack.infra.logs.analysis.logRateSection.table.startTimeLabel', + { defaultMessage: 'Start time' } +); +const anomalyScoreLabel = i18n.translate( + 'xpack.infra.logs.analysis.logRateSection.table.anomalyScoreLabel', + { defaultMessage: 'Anomaly score' } +); +const actualLogEntryRateLabel = i18n.translate( + 'xpack.infra.logs.analysis.logRateSection.table.actualLogEntryRateLabel', + { defaultMessage: 'Actual rate' } +); +const typicalLogEntryRateLabel = i18n.translate( + 'xpack.infra.logs.analysis.logRateSection.table.typicalLogEntryRateLabel', + { defaultMessage: 'Typical rate' } +); + +export const TableView = ({ data }: Props) => { + const [dateFormat] = useKibanaUiSetting('dateFormat'); + + const formattedAnomalies = useMemo(() => { + return data.histogramBuckets.reduce((acc: any, bucket) => { + if (bucket.anomalies.length > 0) { + bucket.anomalies.forEach(anomaly => { + const formattedAnomaly = { + startTime: moment(anomaly.startTime).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'), + anomalyScore: Number(anomaly.anomalyScore).toFixed(3), + typicalLogEntryRate: Number(anomaly.typicalLogEntryRate).toFixed(3), + actualLogEntryRate: Number(anomaly.actualLogEntryRate).toFixed(3), + }; + acc.push(formattedAnomaly); + }); + return acc; + } else { + return acc; + } + }, []); + }, [data]); + + const columns = [ + { + field: 'startTime', + name: startTimeLabel, + sortable: true, + 'data-test-subj': 'startTimeCell', + }, + { + field: 'anomalyScore', + name: anomalyScoreLabel, + sortable: true, + 'data-test-subj': 'anomalyScoreCell', + }, + { + field: 'actualLogEntryRate', + name: actualLogEntryRateLabel, + sortable: true, + 'data-test-subj': 'actualLogEntryRateCell', + }, + { + field: 'typicalLogEntryRate', + name: typicalLogEntryRateLabel, + sortable: true, + 'data-test-subj': 'typicalLogEntryRateCell', + }, + ]; + + const initialSorting = { + sort: { + field: 'anomalyScore', + direction: 'desc', + }, + }; + + return ( + <> + + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/view_switcher.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/view_switcher.tsx new file mode 100644 index 0000000000000..fad866822bb93 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/view_switcher.tsx @@ -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 { EuiButtonGroup, EuiButtonGroupProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ViewMode } from './index'; + +interface Props { + selectedView: string; + onChange: EuiButtonGroupProps['onChange']; +} + +const chartLabel = i18n.translate( + 'xpack.infra.logs.analysis.logRateSection.viewSwitcher.chartLabel', + { defaultMessage: 'Chart view' } +); +const tableLabel = i18n.translate( + 'xpack.infra.logs.analysis.logRateSection.viewSwitcher.tableLabel', + { defaultMessage: 'Table view' } +); +const legendLabel = i18n.translate( + 'xpack.infra.logs.analysis.logRateSection.viewSwitcher.legendLabel', + { defaultMessage: 'Switch between chart and table view' } +); + +export const ViewSwitcher = ({ selectedView, onChange }: Props) => { + const buttons = [ + { + id: ViewMode.chart, + label: chartLabel, + iconType: 'apps', + }, + { + id: ViewMode.table, + label: tableLabel, + iconType: 'editorUnorderedList', + }, + ]; + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx index f2fcabe60e913..a20b1dc9641c9 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx @@ -6,6 +6,7 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiBetaBadge } from '@elastic/eui'; import React from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; import { UICapabilities } from 'ui/capabilities'; @@ -43,11 +44,46 @@ export const LogsPage = injectUICapabilities( }), path: `${match.path}/stream`, }; + const analysisBetaBadgeTitle = i18n.translate('xpack.infra.logs.index.analysisBetaBadgeTitle', { + defaultMessage: 'Analysis', + }); + const analysisBetaBadgeLabel = i18n.translate('xpack.infra.logs.index.analysisBetaBadgeLabel', { + defaultMessage: 'Beta', + }); + const analysisBetaBadgeTooltipContent = i18n.translate( + 'xpack.infra.logs.index.analysisBetaBadgeTooltipContent', + { + defaultMessage: + 'This feature is under active development. Extra functionality is coming, and some functionality may change.', + } + ); + const analysisBetaBadge = ( + + ); const analysisTab = { - title: intl.formatMessage({ - id: 'xpack.infra.logs.index.analysisTabTitle', - defaultMessage: 'Analysis', - }), + title: ( + <> + + {intl.formatMessage({ + id: 'xpack.infra.logs.index.analysisTabTitle', + defaultMessage: 'Analysis', + })} + + {analysisBetaBadge} + + ), path: `${match.path}/analysis`, }; const settingsTab = {