Skip to content

Commit

Permalink
Break into smaller files
Browse files Browse the repository at this point in the history
  • Loading branch information
jen-huang committed Nov 25, 2020
1 parent a574c37 commit 12d1e76
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 279 deletions.
Original file line number Diff line number Diff line change
@@ -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<AgentLogsProps> = 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<string>(state.query);
const [isDraftQueryValid, setIsDraftQueryValid] = useState<boolean>(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 (
<WrapperFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem>
<LogQueryBar
query={draftQuery}
onUpdateQuery={onUpdateDraftQuery}
isQueryValid={isDraftQueryValid}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFilterGroup>
<DatasetFilter
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 {
updateState({ datasets: [...state.datasets, dataset] });
}
}}
/>
<LogLevelFilter
selectedLevels={state.logLevels}
onToggleLevel={(level: string) => {
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] });
}
}}
/>
</EuiFilterGroup>
</EuiFlexItem>
<DatePickerFlexItem grow={false}>
<EuiSuperDatePicker
showUpdateButton={false}
start={state.start}
end={state.end}
onTimeChange={({ start, end }) => {
tryUpdateDateRange({
from: start,
to: end,
});
}}
/>
</DatePickerFlexItem>
<EuiFlexItem grow={false}>
<RedirectAppLinks application={application}>
<EuiButtonEmpty href={viewInLogsUrl} iconType="popout" flush="both">
<FormattedMessage
id="xpack.fleet.agentLogs.openInLogsUiLinkText"
defaultMessage="Open in Logs"
/>
</EuiButtonEmpty>
</RedirectAppLinks>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel paddingSize="none">
<LogStream
height="100%"
startTimestamp={dateRangeTimestamps!.startTimestamp}
endTimestamp={dateRangeTimestamps!.endTimestamp}
query={logStreamQuery}
/>
</EuiPanel>
</EuiFlexItem>
{isLogLevelSelectionAvailable && (
<EuiFlexItem grow={false}>
<SelectLogLevel agent={agent} />
</EuiFlexItem>
)}
</WrapperFlexGroup>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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.*';
Expand All @@ -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',
Expand Down
Loading

0 comments on commit 12d1e76

Please sign in to comment.