From a574c37c451b9b155d202089b9d77204f198e818 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 24 Nov 2020 15:58:13 -0800 Subject: [PATCH 1/3] Initial attempt at URL state --- .../components/agent_logs/index.tsx | 213 ++++++++++++------ 1 file changed, 149 insertions(+), 64 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx index bed857c073099..49c0fd7b85df3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useMemo, useState, useCallback } from 'react'; +import React, { memo, useMemo, useState, useCallback, useEffect } from 'react'; import styled from 'styled-components'; import url from 'url'; import { encode } from 'rison-node'; @@ -19,12 +19,21 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import semverGte from 'semver/functions/gte'; import semverCoerce from 'semver/functions/coerce'; +import { + createStateContainer, + syncState, + createKbnUrlStateStorage, + INullableBaseStateContainer, + createStateContainerReactHelpers, + PureTransition, + getStateFromKbnUrl, +} from '../../../../../../../../../../../src/plugins/kibana_utils/public'; import { RedirectAppLinks } from '../../../../../../../../../../../src/plugins/kibana_react/public'; import { TimeRange, esKuery } from '../../../../../../../../../../../src/plugins/data/public'; import { LogStream } from '../../../../../../../../../infra/public'; import { Agent } from '../../../../../types'; import { useStartServices } from '../../../../../hooks'; -import { AGENT_DATASET, DEFAULT_DATE_RANGE } from './constants'; +import { DEFAULT_DATE_RANGE, AGENT_DATASET } from './constants'; import { DatasetFilter } from './filter_dataset'; import { LogLevelFilter } from './filter_log_level'; import { LogQueryBar } from './query_bar'; @@ -39,8 +48,43 @@ const DatePickerFlexItem = styled(EuiFlexItem)` max-width: 312px; `; -export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agent }) => { +interface AgentLogsProps { + agent: Agent; + state: AgentLogsState; +} + +export interface AgentLogsState { + start: string; + end: string; + logLevels: string[]; + datasets: string[]; + query: string; +} + +const defaultState: AgentLogsState = { + start: DEFAULT_DATE_RANGE.start, + end: DEFAULT_DATE_RANGE.end, + logLevels: [], + datasets: [AGENT_DATASET], + query: '', +}; + +const stateStorageKey = '_q'; + +const stateContainer = createStateContainer< + AgentLogsState, + { + update: PureTransition]>; + } +>(defaultState, { + update: (state) => (updatedState) => ({ ...state, ...updatedState }), +}); + +const AgentLogsUrlStateHelper = createStateContainerReactHelpers(); + +const AgentLogsUI: React.FunctionComponent = memo(({ agent, state }) => { const { data, application, http } = useStartServices(); + const { update: updateState } = AgentLogsUrlStateHelper.useTransitions(); // Util to convert date expressions (returned by datepicker) to timestamps (used by LogStream) const getDateRangeTimestamps = useCallback( @@ -56,63 +100,66 @@ export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agen [data.query.timefilter.timefilter] ); - // Initial time range filter - const [dateRange, setDateRange] = useState<{ - startExpression: string; - endExpression: string; - startTimestamp: number; - endTimestamp: number; - }>({ - startExpression: DEFAULT_DATE_RANGE.start, - endExpression: DEFAULT_DATE_RANGE.end, - ...getDateRangeTimestamps({ from: DEFAULT_DATE_RANGE.start, to: DEFAULT_DATE_RANGE.end })!, - }); - const tryUpdateDateRange = useCallback( (timeRange: TimeRange) => { const timestamps = getDateRangeTimestamps(timeRange); if (timestamps) { - setDateRange({ - startExpression: timeRange.from, - endExpression: timeRange.to, - ...timestamps, + updateState({ + start: timeRange.from, + end: timeRange.to, }); } }, - [getDateRangeTimestamps] + [getDateRangeTimestamps, updateState] ); - // Filters - const [selectedLogLevels, setSelectedLogLevels] = useState([]); - const [selectedDatasets, setSelectedDatasets] = useState([AGENT_DATASET]); + const dateRangeTimestamps = useMemo( + () => + getDateRangeTimestamps({ + from: state.start, + to: state.end, + }), + [getDateRangeTimestamps, state.end, state.start] + ); - // User query state - const [query, setQuery] = useState(''); - const [draftQuery, setDraftQuery] = useState(''); - const [isDraftQueryValid, setIsDraftQueryValid] = useState(true); - const onUpdateDraftQuery = useCallback((newDraftQuery: string, runQuery?: boolean) => { - setDraftQuery(newDraftQuery); + // Query validation helper + const isQueryValid = useCallback((testQuery: string) => { try { - esKuery.fromKueryExpression(newDraftQuery); - setIsDraftQueryValid(true); - if (runQuery) { - setQuery(newDraftQuery); - } + esKuery.fromKueryExpression(testQuery); + return true; } catch (err) { - setIsDraftQueryValid(false); + return false; } }, []); + // User query state + const [draftQuery, setDraftQuery] = useState(state.query); + const [isDraftQueryValid, setIsDraftQueryValid] = useState(isQueryValid(state.query)); + const onUpdateDraftQuery = useCallback( + (newDraftQuery: string, runQuery?: boolean) => { + setDraftQuery(newDraftQuery); + if (isQueryValid(newDraftQuery)) { + setIsDraftQueryValid(true); + if (runQuery) { + updateState({ query: newDraftQuery }); + } + } else { + setIsDraftQueryValid(false); + } + }, + [isQueryValid, updateState] + ); + // Build final log stream query from agent id, datasets, log levels, and user input const logStreamQuery = useMemo( () => buildQuery({ agentId: agent.id, - datasets: selectedDatasets, - logLevels: selectedLogLevels, - userQuery: query, + datasets: state.datasets, + logLevels: state.logLevels, + userQuery: state.query, }), - [agent.id, query, selectedDatasets, selectedLogLevels] + [agent.id, state.datasets, state.logLevels, state.query] ); // Generate URL to pass page state to Logs UI @@ -124,8 +171,8 @@ export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agen search: stringify( { logPosition: encode({ - start: dateRange.startExpression, - end: dateRange.endExpression, + start: state.start, + end: state.end, streamLive: false, }), logFilter: encode({ @@ -137,7 +184,7 @@ export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agen ), }) ), - [logStreamQuery, dateRange.endExpression, dateRange.startExpression, http.basePath] + [http.basePath, state.start, state.end, logStreamQuery] ); const agentVersion = agent.local_metadata?.elastic?.agent?.version; @@ -152,6 +199,18 @@ export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agen return semverGte(agentVersionWithPrerelease, '7.11.0'); }, [agentVersion]); + const logStream = useMemo( + () => ( + + ), + [dateRangeTimestamps, logStreamQuery] + ); + return ( @@ -166,28 +225,28 @@ export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agen { - const currentLevels = [...selectedDatasets]; - const levelPosition = currentLevels.indexOf(level); - if (levelPosition >= 0) { - currentLevels.splice(levelPosition, 1); - setSelectedDatasets(currentLevels); + selectedDatasets={state.datasets} + onToggleDataset={(dataset: string) => { + const currentDatasets = [...state.datasets]; + const datasetPosition = currentDatasets.indexOf(dataset); + if (datasetPosition >= 0) { + currentDatasets.splice(datasetPosition, 1); + updateState({ datasets: currentDatasets }); } else { - setSelectedDatasets([...selectedDatasets, level]); + updateState({ datasets: [...state.datasets, dataset] }); } }} /> { - const currentLevels = [...selectedLogLevels]; + const currentLevels = [...state.logLevels]; const levelPosition = currentLevels.indexOf(level); if (levelPosition >= 0) { currentLevels.splice(levelPosition, 1); - setSelectedLogLevels(currentLevels); + updateState({ logLevels: currentLevels }); } else { - setSelectedLogLevels([...selectedLogLevels, level]); + updateState({ logLevels: [...state.logLevels, level] }); } }} /> @@ -196,8 +255,8 @@ export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agen { tryUpdateDateRange({ from: start, @@ -219,14 +278,7 @@ export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agen - - - + {logStream} {isLogLevelSelectionAvailable && ( @@ -236,3 +288,36 @@ export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agen ); }); + +const AgentLogsConnected = AgentLogsUrlStateHelper.connect((state) => ({ + state: state || defaultState, +}))(AgentLogsUI); + +export const AgentLogs: React.FunctionComponent> = memo( + ({ ...props }) => { + useEffect(() => { + stateContainer.set({ + ...defaultState, + ...getStateFromKbnUrl(stateStorageKey, window.location.href), + }); + const stateStorage = createKbnUrlStateStorage(); + const { start, stop } = syncState({ + storageKey: stateStorageKey, + stateContainer: stateContainer as INullableBaseStateContainer, + stateStorage, + }); + start(); + + return () => { + stop(); + stateContainer.set(defaultState); + }; + }, []); + + return ( + + + + ); + } +); From 12d1e761c13509463b68c6412d040686bcd10b16 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 24 Nov 2020 16:31:20 -0800 Subject: [PATCH 2/3] Break into smaller files --- .../components/agent_logs/agent_logs.tsx | 257 +++++++++++++++ .../components/agent_logs/constants.tsx | 9 + .../components/agent_logs/index.tsx | 297 ++---------------- 3 files changed, 284 insertions(+), 279 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx new file mode 100644 index 0000000000000..cce76b0bdbe80 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -0,0 +1,257 @@ +/* + * 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, { memo, useMemo, useState, useCallback } from 'react'; +import styled from 'styled-components'; +import url from 'url'; +import { encode } from 'rison-node'; +import { stringify } from 'query-string'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSuperDatePicker, + EuiFilterGroup, + EuiPanel, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import semverGte from 'semver/functions/gte'; +import semverCoerce from 'semver/functions/coerce'; +import { createStateContainerReactHelpers } from '../../../../../../../../../../../src/plugins/kibana_utils/public'; +import { RedirectAppLinks } from '../../../../../../../../../../../src/plugins/kibana_react/public'; +import { TimeRange, esKuery } from '../../../../../../../../../../../src/plugins/data/public'; +import { LogStream } from '../../../../../../../../../infra/public'; +import { Agent } from '../../../../../types'; +import { useStartServices } from '../../../../../hooks'; +import { DatasetFilter } from './filter_dataset'; +import { LogLevelFilter } from './filter_log_level'; +import { LogQueryBar } from './query_bar'; +import { buildQuery } from './build_query'; +import { SelectLogLevel } from './select_log_level'; + +const WrapperFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + +const DatePickerFlexItem = styled(EuiFlexItem)` + max-width: 312px; +`; + +export interface AgentLogsProps { + agent: Agent; + state: AgentLogsState; +} + +export interface AgentLogsState { + start: string; + end: string; + logLevels: string[]; + datasets: string[]; + query: string; +} + +export const AgentLogsUrlStateHelper = createStateContainerReactHelpers(); + +export const AgentLogsUI: React.FunctionComponent = memo(({ agent, state }) => { + const { data, application, http } = useStartServices(); + const { update: updateState } = AgentLogsUrlStateHelper.useTransitions(); + + // Util to convert date expressions (returned by datepicker) to timestamps (used by LogStream) + const getDateRangeTimestamps = useCallback( + (timeRange: TimeRange) => { + const { min, max } = data.query.timefilter.timefilter.calculateBounds(timeRange); + return min && max + ? { + startTimestamp: min.valueOf(), + endTimestamp: max.valueOf(), + } + : undefined; + }, + [data.query.timefilter.timefilter] + ); + + const tryUpdateDateRange = useCallback( + (timeRange: TimeRange) => { + const timestamps = getDateRangeTimestamps(timeRange); + if (timestamps) { + updateState({ + start: timeRange.from, + end: timeRange.to, + }); + } + }, + [getDateRangeTimestamps, updateState] + ); + + const dateRangeTimestamps = useMemo( + () => + getDateRangeTimestamps({ + from: state.start, + to: state.end, + }), + [getDateRangeTimestamps, state.end, state.start] + ); + + // Query validation helper + const isQueryValid = useCallback((testQuery: string) => { + try { + esKuery.fromKueryExpression(testQuery); + return true; + } catch (err) { + return false; + } + }, []); + + // User query state + const [draftQuery, setDraftQuery] = useState(state.query); + const [isDraftQueryValid, setIsDraftQueryValid] = useState(isQueryValid(state.query)); + const onUpdateDraftQuery = useCallback( + (newDraftQuery: string, runQuery?: boolean) => { + setDraftQuery(newDraftQuery); + if (isQueryValid(newDraftQuery)) { + setIsDraftQueryValid(true); + if (runQuery) { + updateState({ query: newDraftQuery }); + } + } else { + setIsDraftQueryValid(false); + } + }, + [isQueryValid, updateState] + ); + + // Build final log stream query from agent id, datasets, log levels, and user input + const logStreamQuery = useMemo( + () => + buildQuery({ + agentId: agent.id, + datasets: state.datasets, + logLevels: state.logLevels, + userQuery: state.query, + }), + [agent.id, state.datasets, state.logLevels, state.query] + ); + + // Generate URL to pass page state to Logs UI + const viewInLogsUrl = useMemo( + () => + http.basePath.prepend( + url.format({ + pathname: '/app/logs/stream', + search: stringify( + { + logPosition: encode({ + start: state.start, + end: state.end, + streamLive: false, + }), + logFilter: encode({ + expression: logStreamQuery, + kind: 'kuery', + }), + }, + { sort: false, encode: false } + ), + }) + ), + [http.basePath, state.start, state.end, logStreamQuery] + ); + + const agentVersion = agent.local_metadata?.elastic?.agent?.version; + const isLogLevelSelectionAvailable = useMemo(() => { + if (!agentVersion) { + return false; + } + const agentVersionWithPrerelease = semverCoerce(agentVersion)?.version; + if (!agentVersionWithPrerelease) { + return false; + } + return semverGte(agentVersionWithPrerelease, '7.11.0'); + }, [agentVersion]); + + return ( + + + + + + + + + { + const currentDatasets = [...state.datasets]; + const datasetPosition = currentDatasets.indexOf(dataset); + if (datasetPosition >= 0) { + currentDatasets.splice(datasetPosition, 1); + updateState({ datasets: currentDatasets }); + } else { + updateState({ datasets: [...state.datasets, dataset] }); + } + }} + /> + { + const currentLevels = [...state.logLevels]; + const levelPosition = currentLevels.indexOf(level); + if (levelPosition >= 0) { + currentLevels.splice(levelPosition, 1); + updateState({ logLevels: currentLevels }); + } else { + updateState({ logLevels: [...state.logLevels, level] }); + } + }} + /> + + + + { + tryUpdateDateRange({ + from: start, + to: end, + }); + }} + /> + + + + + + + + + + + + + + + + {isLogLevelSelectionAvailable && ( + + + + )} + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx index 41069e7107862..92bf082513370 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { AgentLogsState } from './agent_logs'; + export const AGENT_LOG_INDEX_PATTERN = 'logs-elastic_agent-*,logs-elastic_agent.*-*'; export const AGENT_DATASET = 'elastic_agent'; export const AGENT_DATASET_PATTERN = 'elastic_agent.*'; @@ -24,6 +26,13 @@ export const DEFAULT_DATE_RANGE = { start: 'now-1d', end: 'now', }; +export const DEFAULT_LOGS_STATE: AgentLogsState = { + start: DEFAULT_DATE_RANGE.start, + end: DEFAULT_DATE_RANGE.end, + logLevels: [], + datasets: [AGENT_DATASET], + query: '', +}; export const AGENT_LOG_LEVELS = { ERROR: 'error', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx index 49c0fd7b85df3..fd685efffc92b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx @@ -3,71 +3,17 @@ * 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, { memo, useMemo, useState, useCallback, useEffect } from 'react'; -import styled from 'styled-components'; -import url from 'url'; -import { encode } from 'rison-node'; -import { stringify } from 'query-string'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSuperDatePicker, - EuiFilterGroup, - EuiPanel, - EuiButtonEmpty, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import semverGte from 'semver/functions/gte'; -import semverCoerce from 'semver/functions/coerce'; +import React, { memo, useEffect } from 'react'; import { createStateContainer, syncState, createKbnUrlStateStorage, INullableBaseStateContainer, - createStateContainerReactHelpers, PureTransition, getStateFromKbnUrl, } from '../../../../../../../../../../../src/plugins/kibana_utils/public'; -import { RedirectAppLinks } from '../../../../../../../../../../../src/plugins/kibana_react/public'; -import { TimeRange, esKuery } from '../../../../../../../../../../../src/plugins/data/public'; -import { LogStream } from '../../../../../../../../../infra/public'; -import { Agent } from '../../../../../types'; -import { useStartServices } from '../../../../../hooks'; -import { DEFAULT_DATE_RANGE, AGENT_DATASET } from './constants'; -import { DatasetFilter } from './filter_dataset'; -import { LogLevelFilter } from './filter_log_level'; -import { LogQueryBar } from './query_bar'; -import { buildQuery } from './build_query'; -import { SelectLogLevel } from './select_log_level'; - -const WrapperFlexGroup = styled(EuiFlexGroup)` - height: 100%; -`; - -const DatePickerFlexItem = styled(EuiFlexItem)` - max-width: 312px; -`; - -interface AgentLogsProps { - agent: Agent; - state: AgentLogsState; -} - -export interface AgentLogsState { - start: string; - end: string; - logLevels: string[]; - datasets: string[]; - query: string; -} - -const defaultState: AgentLogsState = { - start: DEFAULT_DATE_RANGE.start, - end: DEFAULT_DATE_RANGE.end, - logLevels: [], - datasets: [AGENT_DATASET], - query: '', -}; +import { DEFAULT_LOGS_STATE } from './constants'; +import { AgentLogsUI, AgentLogsProps, AgentLogsState, AgentLogsUrlStateHelper } from './agent_logs'; const stateStorageKey = '_q'; @@ -76,230 +22,23 @@ const stateContainer = createStateContainer< { update: PureTransition]>; } ->(defaultState, { - update: (state) => (updatedState) => ({ ...state, ...updatedState }), -}); - -const AgentLogsUrlStateHelper = createStateContainerReactHelpers(); - -const AgentLogsUI: React.FunctionComponent = memo(({ agent, state }) => { - const { data, application, http } = useStartServices(); - const { update: updateState } = AgentLogsUrlStateHelper.useTransitions(); - - // Util to convert date expressions (returned by datepicker) to timestamps (used by LogStream) - const getDateRangeTimestamps = useCallback( - (timeRange: TimeRange) => { - const { min, max } = data.query.timefilter.timefilter.calculateBounds(timeRange); - return min && max - ? { - startTimestamp: min.valueOf(), - endTimestamp: max.valueOf(), - } - : undefined; - }, - [data.query.timefilter.timefilter] - ); - - const tryUpdateDateRange = useCallback( - (timeRange: TimeRange) => { - const timestamps = getDateRangeTimestamps(timeRange); - if (timestamps) { - updateState({ - start: timeRange.from, - end: timeRange.to, - }); - } - }, - [getDateRangeTimestamps, updateState] - ); - - const dateRangeTimestamps = useMemo( - () => - getDateRangeTimestamps({ - from: state.start, - to: state.end, - }), - [getDateRangeTimestamps, state.end, state.start] - ); - - // Query validation helper - const isQueryValid = useCallback((testQuery: string) => { - try { - esKuery.fromKueryExpression(testQuery); - return true; - } catch (err) { - return false; - } - }, []); - - // User query state - const [draftQuery, setDraftQuery] = useState(state.query); - const [isDraftQueryValid, setIsDraftQueryValid] = useState(isQueryValid(state.query)); - const onUpdateDraftQuery = useCallback( - (newDraftQuery: string, runQuery?: boolean) => { - setDraftQuery(newDraftQuery); - if (isQueryValid(newDraftQuery)) { - setIsDraftQueryValid(true); - if (runQuery) { - updateState({ query: newDraftQuery }); - } - } else { - setIsDraftQueryValid(false); - } - }, - [isQueryValid, updateState] - ); - - // Build final log stream query from agent id, datasets, log levels, and user input - const logStreamQuery = useMemo( - () => - buildQuery({ - agentId: agent.id, - datasets: state.datasets, - logLevels: state.logLevels, - userQuery: state.query, - }), - [agent.id, state.datasets, state.logLevels, state.query] - ); - - // Generate URL to pass page state to Logs UI - const viewInLogsUrl = useMemo( - () => - http.basePath.prepend( - url.format({ - pathname: '/app/logs/stream', - search: stringify( - { - logPosition: encode({ - start: state.start, - end: state.end, - streamLive: false, - }), - logFilter: encode({ - expression: logStreamQuery, - kind: 'kuery', - }), - }, - { sort: false, encode: false } - ), - }) - ), - [http.basePath, state.start, state.end, logStreamQuery] - ); - - const agentVersion = agent.local_metadata?.elastic?.agent?.version; - const isLogLevelSelectionAvailable = useMemo(() => { - if (!agentVersion) { - return false; - } - const agentVersionWithPrerelease = semverCoerce(agentVersion)?.version; - if (!agentVersionWithPrerelease) { - return false; - } - return semverGte(agentVersionWithPrerelease, '7.11.0'); - }, [agentVersion]); - - const logStream = useMemo( - () => ( - - ), - [dateRangeTimestamps, logStreamQuery] - ); - - return ( - - - - - - - - - { - const currentDatasets = [...state.datasets]; - const datasetPosition = currentDatasets.indexOf(dataset); - if (datasetPosition >= 0) { - currentDatasets.splice(datasetPosition, 1); - updateState({ datasets: currentDatasets }); - } else { - updateState({ datasets: [...state.datasets, dataset] }); - } - }} - /> - { - const currentLevels = [...state.logLevels]; - const levelPosition = currentLevels.indexOf(level); - if (levelPosition >= 0) { - currentLevels.splice(levelPosition, 1); - updateState({ logLevels: currentLevels }); - } else { - updateState({ logLevels: [...state.logLevels, level] }); - } - }} - /> - - - - { - tryUpdateDateRange({ - from: start, - to: end, - }); - }} - /> - - - - - - - - - - - - {logStream} - - {isLogLevelSelectionAvailable && ( - - - - )} - - ); -}); +>( + { + ...DEFAULT_LOGS_STATE, + ...getStateFromKbnUrl(stateStorageKey, window.location.href), + }, + { + update: (state) => (updatedState) => ({ ...state, ...updatedState }), + } +); -const AgentLogsConnected = AgentLogsUrlStateHelper.connect((state) => ({ - state: state || defaultState, +const AgentLogsConnected = AgentLogsUrlStateHelper.connect((state) => ({ + state: state || DEFAULT_LOGS_STATE, }))(AgentLogsUI); -export const AgentLogs: React.FunctionComponent> = memo( - ({ ...props }) => { +export const AgentLogs: React.FunctionComponent> = memo( + ({ agent }) => { useEffect(() => { - stateContainer.set({ - ...defaultState, - ...getStateFromKbnUrl(stateStorageKey, window.location.href), - }); const stateStorage = createKbnUrlStateStorage(); const { start, stop } = syncState({ storageKey: stateStorageKey, @@ -310,13 +49,13 @@ export const AgentLogs: React.FunctionComponent return () => { stop(); - stateContainer.set(defaultState); + stateContainer.set(DEFAULT_LOGS_STATE); }; }, []); return ( - + ); } From 0e31368f42b98c5555a857cbe93530ba6554afd4 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 25 Nov 2020 10:57:53 -0800 Subject: [PATCH 3/3] Handle invalid date range expressions --- .../components/agent_logs/agent_logs.tsx | 43 ++++++++++++++----- .../components/agent_logs/index.tsx | 7 ++- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index cce76b0bdbe80..00deeff89503f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useMemo, useState, useCallback } from 'react'; +import React, { memo, useMemo, useState, useCallback, useEffect } from 'react'; import styled from 'styled-components'; import url from 'url'; import { encode } from 'rison-node'; @@ -25,6 +25,7 @@ import { TimeRange, esKuery } from '../../../../../../../../../../../src/plugins import { LogStream } from '../../../../../../../../../infra/public'; import { Agent } from '../../../../../types'; import { useStartServices } from '../../../../../hooks'; +import { DEFAULT_DATE_RANGE } from './constants'; import { DatasetFilter } from './filter_dataset'; import { LogLevelFilter } from './filter_log_level'; import { LogQueryBar } from './query_bar'; @@ -64,8 +65,8 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen const { min, max } = data.query.timefilter.timefilter.calculateBounds(timeRange); return min && max ? { - startTimestamp: min.valueOf(), - endTimestamp: max.valueOf(), + start: min.valueOf(), + end: max.valueOf(), } : undefined; }, @@ -85,15 +86,35 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen [getDateRangeTimestamps, updateState] ); - const dateRangeTimestamps = useMemo( - () => + const [dateRangeTimestamps, setDateRangeTimestamps] = useState<{ start: number; end: number }>( + getDateRangeTimestamps({ + from: state.start, + to: state.end, + }) || getDateRangeTimestamps({ - from: state.start, - to: state.end, - }), - [getDateRangeTimestamps, state.end, state.start] + from: DEFAULT_DATE_RANGE.start, + to: DEFAULT_DATE_RANGE.end, + })! ); + // Attempts to parse for timestamps when start/end date expressions change + // If invalid date expressions, set expressions back to default + // Otherwise set the new timestamps + useEffect(() => { + const timestampsFromDateRange = getDateRangeTimestamps({ + from: state.start, + to: state.end, + }); + if (!timestampsFromDateRange) { + tryUpdateDateRange({ + from: DEFAULT_DATE_RANGE.start, + to: DEFAULT_DATE_RANGE.end, + }); + } else { + setDateRangeTimestamps(timestampsFromDateRange); + } + }, [state.start, state.end, getDateRangeTimestamps, tryUpdateDateRange]); + // Query validation helper const isQueryValid = useCallback((testQuery: string) => { try { @@ -241,8 +262,8 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx index fd685efffc92b..0d888a88ec2cb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useEffect } from 'react'; +import React, { memo, useEffect, useState } from 'react'; import { createStateContainer, syncState, @@ -38,6 +38,8 @@ const AgentLogsConnected = AgentLogsUrlStateHelper.connect> = memo( ({ agent }) => { + const [isSyncReady, setIsSyncReady] = useState(false); + useEffect(() => { const stateStorage = createKbnUrlStateStorage(); const { start, stop } = syncState({ @@ -46,6 +48,7 @@ export const AgentLogs: React.FunctionComponent> = stateStorage, }); start(); + setIsSyncReady(true); return () => { stop(); @@ -55,7 +58,7 @@ export const AgentLogs: React.FunctionComponent> = return ( - + {isSyncReady ? : null} ); }