[InfraOps] Preserve time values when navigating from the waffle map to the logs and details pages (#24666)

This adds `time` and `from`/`to` parameters to the metrics and logs links of node context menu in the waffle map.
This commit is contained in:
Felix Stürmer 2018-10-31 15:57:53 +01:00 committed by GitHub
parent 6ac9a2fd23
commit c8b2e673fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 132 additions and 268 deletions

View file

@ -6,7 +6,7 @@
import React from 'react';
import styled from 'styled-components';
import { InfraNodeType } from '../../../common/graphql/types';
import { InfraNodeType, InfraTimerangeInput } from '../../../common/graphql/types';
import {
InfraWaffleMapBounds,
InfraWaffleMapGroupOfGroups,
@ -22,6 +22,7 @@ interface Props {
formatter: (val: number) => string;
bounds: InfraWaffleMapBounds;
nodeType: InfraNodeType;
timeRange: InfraTimerangeInput;
}
export const GroupOfGroups: React.SFC<Props> = props => {
@ -39,6 +40,7 @@ export const GroupOfGroups: React.SFC<Props> = props => {
formatter={props.formatter}
bounds={props.bounds}
nodeType={props.nodeType}
timeRange={props.timeRange}
/>
))}
</Groups>

View file

@ -6,7 +6,7 @@
import React from 'react';
import styled from 'styled-components';
import { InfraNodeType } from '../../../common/graphql/types';
import { InfraNodeType, InfraTimerangeInput } from '../../../common/graphql/types';
import {
InfraWaffleMapBounds,
InfraWaffleMapGroupOfNodes,
@ -23,6 +23,7 @@ interface Props {
isChild: boolean;
bounds: InfraWaffleMapBounds;
nodeType: InfraNodeType;
timeRange: InfraTimerangeInput;
}
export const GroupOfNodes: React.SFC<Props> = ({
@ -33,6 +34,7 @@ export const GroupOfNodes: React.SFC<Props> = ({
isChild = false,
bounds,
nodeType,
timeRange,
}) => {
const width = group.width > 200 ? group.width : 200;
return (
@ -48,6 +50,7 @@ export const GroupOfNodes: React.SFC<Props> = ({
formatter={formatter}
bounds={bounds}
nodeType={nodeType}
timeRange={timeRange}
/>
))}
</Nodes>

View file

@ -7,7 +7,7 @@ import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { get, max, min } from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { InfraMetricType, InfraNodeType } from '../../../common/graphql/types';
import { InfraMetricType, InfraNodeType, InfraTimerangeInput } from '../../../common/graphql/types';
import {
isWaffleMapGroupWithGroups,
isWaffleMapGroupWithNodes,
@ -35,6 +35,7 @@ interface Props {
loading: boolean;
reload: () => void;
onDrilldown: (filter: KueryFilterQuery) => void;
timeRange: InfraTimerangeInput;
}
interface MetricFormatter {
@ -90,7 +91,7 @@ const calculateBoundsFromMap = (map: InfraWaffleData): InfraWaffleMapBounds => {
export class Waffle extends React.Component<Props, {}> {
public render() {
const { loading, map, reload } = this.props;
const { loading, map, reload, timeRange } = this.props;
if (loading) {
return <InfraLoadingPanel height="100%" width="100%" text="Loading data" />;
} else if (!loading && map && map.length === 0) {
@ -132,7 +133,7 @@ export class Waffle extends React.Component<Props, {}> {
data-test-subj="waffleMap"
>
<WaffleMapInnerContainer>
{groupsWithLayout.map(this.renderGroup(bounds))}
{groupsWithLayout.map(this.renderGroup(bounds, timeRange))}
</WaffleMapInnerContainer>
<Legend
formatter={this.formatter}
@ -169,7 +170,9 @@ export class Waffle extends React.Component<Props, {}> {
return;
};
private renderGroup = (bounds: InfraWaffleMapBounds) => (group: InfraWaffleMapGroup) => {
private renderGroup = (bounds: InfraWaffleMapBounds, timeRange: InfraTimerangeInput) => (
group: InfraWaffleMapGroup
) => {
if (isWaffleMapGroupWithGroups(group)) {
return (
<GroupOfGroups
@ -180,6 +183,7 @@ export class Waffle extends React.Component<Props, {}> {
formatter={this.formatter}
bounds={bounds}
nodeType={this.props.nodeType}
timeRange={timeRange}
/>
);
}
@ -194,6 +198,7 @@ export class Waffle extends React.Component<Props, {}> {
isChild={false}
bounds={bounds}
nodeType={this.props.nodeType}
timeRange={timeRange}
/>
);
}

View file

@ -8,6 +8,7 @@ import { EuiToolTip } from '@elastic/eui';
import { darken, readableColor } from 'polished';
import React from 'react';
import styled from 'styled-components';
import { InfraTimerangeInput } from 'x-pack/plugins/infra/common/graphql/types';
import { InfraNodeType } from '../../../server/lib/adapters/nodes';
import { InfraWaffleMapBounds, InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib';
import { colorFromValue } from './lib/color_from_value';
@ -26,12 +27,13 @@ interface Props {
formatter: (val: number) => string;
bounds: InfraWaffleMapBounds;
nodeType: InfraNodeType;
timeRange: InfraTimerangeInput;
}
export class Node extends React.PureComponent<Props, State> {
public readonly state: State = initialState;
public render() {
const { nodeType, node, options, squareSize, bounds, formatter } = this.props;
const { nodeType, node, options, squareSize, bounds, formatter, timeRange } = this.props;
const { isPopoverOpen } = this.state;
const { metric } = node;
const valueMode = squareSize > 110;
@ -45,6 +47,7 @@ export class Node extends React.PureComponent<Props, State> {
isPopoverOpen={isPopoverOpen}
closePopover={this.closePopover}
options={options}
timeRange={timeRange}
>
<EuiToolTip position="top" content={`${node.name} | ${value}`}>
<NodeContainer

View file

@ -6,19 +6,14 @@
import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiPopover } from '@elastic/eui';
import React from 'react';
import { InfraNodeType } from '../../../common/graphql/types';
import { InfraNodeType, InfraTimerangeInput } from '../../../common/graphql/types';
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib';
import {
getContainerDetailUrl,
getContainerLogsUrl,
getHostDetailUrl,
getHostLogsUrl,
getPodDetailUrl,
getPodLogsUrl,
} from '../../pages/link_to';
import { getNodeDetailUrl, getNodeLogsUrl } from '../../pages/link_to';
interface Props {
options: InfraWaffleMapOptions;
timeRange: InfraTimerangeInput;
node: InfraWaffleMapNode;
nodeType: InfraNodeType;
isPopoverOpen: boolean;
@ -27,14 +22,30 @@ interface Props {
export const NodeContextMenu: React.SFC<Props> = ({
options,
timeRange,
children,
node,
isPopoverOpen,
closePopover,
nodeType,
}) => {
const nodeLogsUrl = getNodeLogsUrl(nodeType, node);
const nodeDetailUrl = getNodeDetailUrl(nodeType, node);
const nodeName = node.path.length > 0 ? node.path[node.path.length - 1].value : undefined;
const nodeLogsUrl = nodeName
? getNodeLogsUrl({
nodeType,
nodeName,
time: timeRange.to,
})
: undefined;
const nodeDetailUrl = nodeName
? getNodeDetailUrl({
nodeType,
nodeName,
from: timeRange.from,
to: timeRange.to,
})
: undefined;
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
@ -72,47 +83,3 @@ export const NodeContextMenu: React.SFC<Props> = ({
</EuiPopover>
);
};
const getNodeLogsUrl = (
nodeType: 'host' | 'container' | 'pod',
{ path }: InfraWaffleMapNode
): string | undefined => {
if (path.length <= 0) {
return undefined;
}
const lastPathSegment = path[path.length - 1];
switch (nodeType) {
case 'host':
return getHostLogsUrl({ hostname: lastPathSegment.value });
case 'container':
return getContainerLogsUrl({ containerId: lastPathSegment.value });
case 'pod':
return getPodLogsUrl({ podId: lastPathSegment.value });
default:
return undefined;
}
};
const getNodeDetailUrl = (
nodeType: 'host' | 'container' | 'pod',
{ path }: InfraWaffleMapNode
): string | undefined => {
if (path.length <= 0) {
return undefined;
}
const lastPathSegment = path[path.length - 1];
switch (nodeType) {
case 'host':
return getHostDetailUrl({ name: lastPathSegment.value });
case 'container':
return getContainerDetailUrl({ name: lastPathSegment.value });
case 'pod':
return getPodDetailUrl({ name: lastPathSegment.value });
default:
return undefined;
}
};

View file

@ -11,7 +11,7 @@ import { createSelector } from 'reselect';
import { metricTimeActions, metricTimeSelectors, State } from '../../store';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { UrlStateContainer } from '../../utils/url_state';
import { replaceStateKeyInQueryString, UrlStateContainer } from '../../utils/url_state';
export const withMetricsTime = connect(
(state: State) => ({
@ -94,3 +94,15 @@ const mapToTimeUrlState = (value: any) =>
value && (typeof value.to === 'number' && typeof value.from === 'number') ? value : undefined;
const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined);
export const replaceMetricTimeInQueryString = (from: number, to: number) =>
Number.isNaN(from) || Number.isNaN(to)
? (value: string) => value
: replaceStateKeyInQueryString<MetricTimeUrlState>('metricTime', {
autoReload: false,
time: {
interval: '>=1m',
from,
to,
},
});

View file

@ -44,6 +44,7 @@ export const HomePageContent: React.SFC = () => (
options={{ ...wafflemap, metric, fields: configuredFields, groupBy }}
reload={refetch}
onDrilldown={applyFilterQuery}
timeRange={currentTimeRange}
/>
)}
</WithWaffleNodes>

View file

@ -1,16 +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 { Location } from 'history';
import { replaceStateKeyInQueryString } from '../../utils/url_state';
import { getFromFromLocation, getToFromLocation } from './query_params';
export const createQueryStringForDetailTime = (location: Location) => {
const to = getToFromLocation(location);
const from = getFromFromLocation(location);
return to && from
? '?' + replaceStateKeyInQueryString('metricTime', { to, from, interval: '>=1m' })('')
: '';
};

View file

@ -5,9 +5,5 @@
*/
export { LinkToPage } from './link_to';
export { getContainerLogsUrl, RedirectToContainerLogs } from './redirect_to_container_logs';
export { getHostLogsUrl, RedirectToHostLogs } from './redirect_to_host_logs';
export { getPodLogsUrl, RedirectToPodLogs } from './redirect_to_pod_logs';
export { getHostDetailUrl, RedirectToHostDetail } from './redirect_to_host_detail';
export { getContainerDetailUrl, RedirectToContainerDetail } from './redirect_to_container_detail';
export { getPodDetailUrl, RedirectToPodDetail } from './redirect_to_pod_detail';
export { getNodeLogsUrl, RedirectToNodeLogs } from './redirect_to_node_logs';
export { getNodeDetailUrl, RedirectToNodeDetail } from './redirect_to_node_detail';

View file

@ -7,12 +7,8 @@
import React from 'react';
import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom';
import { RedirectToContainerDetail } from './redirect_to_container_detail';
import { RedirectToContainerLogs } from './redirect_to_container_logs';
import { RedirectToHostDetail } from './redirect_to_host_detail';
import { RedirectToHostLogs } from './redirect_to_host_logs';
import { RedirectToPodDetail } from './redirect_to_pod_detail';
import { RedirectToPodLogs } from './redirect_to_pod_logs';
import { RedirectToNodeDetail } from './redirect_to_node_detail';
import { RedirectToNodeLogs } from './redirect_to_node_logs';
interface LinkToPageProps {
match: RouteMatch<{}>;
@ -25,14 +21,13 @@ export class LinkToPage extends React.Component<LinkToPageProps> {
return (
<Switch>
<Route
path={`${match.url}/container-logs/:containerId`}
component={RedirectToContainerLogs}
path={`${match.url}/:nodeType(host|container|pod)-logs/:nodeName`}
component={RedirectToNodeLogs}
/>
<Route
path={`${match.url}/:nodeType(host|container|pod)-detail/:nodeName`}
component={RedirectToNodeDetail}
/>
<Route path={`${match.url}/host-logs/:hostname`} component={RedirectToHostLogs} />
<Route path={`${match.url}/pod-logs/:podId`} component={RedirectToPodLogs} />
<Route path={`${match.url}/host-detail/:name`} component={RedirectToHostDetail} />
<Route path={`${match.url}/pod-detail/:name`} component={RedirectToPodDetail} />
<Route path={`${match.url}/container-detail/:name`} component={RedirectToContainerDetail} />
<Redirect to="/home" />
</Switch>
);

View file

@ -1,30 +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 from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { createQueryStringForDetailTime } from './create_query_string_for_detail_time';
export const RedirectToContainerDetail = ({
match,
location,
}: RouteComponentProps<{ name: string }>) => {
const args = createQueryStringForDetailTime(location);
return <Redirect to={`/metrics/container/${match.params.name}${args}`} />;
};
export const getContainerDetailUrl = ({
name,
to,
from,
}: {
name: string;
to?: number;
from?: number;
}) => {
const args = to && from ? `?to=${to}&from=${from}` : '';
return `#/link-to/container-detail/${name}${args}`;
};

View file

@ -1,29 +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 from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { createQueryStringForDetailTime } from './create_query_string_for_detail_time';
export const RedirectToHostDetail = ({
match,
location,
}: RouteComponentProps<{ name: string }>) => {
const args = createQueryStringForDetailTime(location);
return <Redirect to={`/metrics/host/${match.params.name}${args}`} />;
};
export const getHostDetailUrl = ({
name,
to,
from,
}: {
name: string;
to?: number;
from?: number;
}) => {
const args = to && from ? `?to=${to}&from=${from}` : '';
return `#/link-to/host-detail/${name}${args}`;
};

View file

@ -1,38 +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 compose from 'lodash/fp/compose';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { LoadingPage } from '../../components/loading_page';
import { replaceLogFilterInQueryString } from '../../containers/logs/with_log_filter';
import { replaceLogPositionInQueryString } from '../../containers/logs/with_log_position';
import { WithSource } from '../../containers/with_source';
import { getTimeFromLocation } from './query_params';
export const RedirectToHostLogs = ({
match,
location,
}: RouteComponentProps<{ hostname: string }>) => (
<WithSource>
{({ configuredFields }) => {
if (!configuredFields) {
return <LoadingPage message="Loading host logs" />;
}
const searchString = compose(
replaceLogFilterInQueryString(`${configuredFields.host}: ${match.params.hostname}`),
replaceLogPositionInQueryString(getTimeFromLocation(location))
)('');
return <Redirect to={`/logs?${searchString}`} />;
}}
</WithSource>
);
export const getHostLogsUrl = ({ hostname, time }: { hostname: string; time?: number }) =>
['#/link-to/host-logs/', hostname, ...(time ? [`?time=${time}`] : [])].join('');

View file

@ -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 React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { InfraNodeType } from 'x-pack/plugins/infra/common/graphql/types';
import { replaceMetricTimeInQueryString } from '../../containers/metrics/with_metrics_time';
import { getFromFromLocation, getToFromLocation } from './query_params';
type RedirectToNodeDetailProps = RouteComponentProps<{
nodeName: string;
nodeType: InfraNodeType;
}>;
export const RedirectToNodeDetail = ({
match: {
params: { nodeName, nodeType },
},
location,
}: RedirectToNodeDetailProps) => {
const searchString = replaceMetricTimeInQueryString(
getFromFromLocation(location),
getToFromLocation(location)
)('');
return <Redirect to={`/metrics/${nodeType}/${nodeName}?${searchString}`} />;
};
export const getNodeDetailUrl = ({
nodeType,
nodeName,
to,
from,
}: {
nodeType: InfraNodeType;
nodeName: string;
to?: number;
from?: number;
}) => {
const args = to && from ? `?to=${to}&from=${from}` : '';
return `#/link-to/${nodeType}-detail/${nodeName}${args}`;
};

View file

@ -8,24 +8,32 @@ import compose from 'lodash/fp/compose';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { InfraNodeType } from 'x-pack/plugins/infra/common/graphql/types';
import { LoadingPage } from '../../components/loading_page';
import { replaceLogFilterInQueryString } from '../../containers/logs/with_log_filter';
import { replaceLogPositionInQueryString } from '../../containers/logs/with_log_position';
import { WithSource } from '../../containers/with_source';
import { getTimeFromLocation } from './query_params';
export const RedirectToContainerLogs = ({
match,
type RedirectToNodeLogsProps = RouteComponentProps<{
nodeName: string;
nodeType: InfraNodeType;
}>;
export const RedirectToNodeLogs = ({
match: {
params: { nodeName, nodeType },
},
location,
}: RouteComponentProps<{ containerId: string }>) => (
}: RedirectToNodeLogsProps) => (
<WithSource>
{({ configuredFields }) => {
if (!configuredFields) {
return <LoadingPage message="Loading container logs" />;
return <LoadingPage message={`Loading ${nodeType} logs`} />;
}
const searchString = compose(
replaceLogFilterInQueryString(`${configuredFields.container}: ${match.params.containerId}`),
replaceLogFilterInQueryString(`${configuredFields[nodeType]}: ${nodeName}`),
replaceLogPositionInQueryString(getTimeFromLocation(location))
)('');
@ -34,10 +42,12 @@ export const RedirectToContainerLogs = ({
</WithSource>
);
export const getContainerLogsUrl = ({
containerId,
export const getNodeLogsUrl = ({
nodeName,
nodeType,
time,
}: {
containerId: string;
nodeName: string;
nodeType: InfraNodeType;
time?: number;
}) => ['#/link-to/container-logs/', containerId, ...(time ? [`?time=${time}`] : [])].join('');
}) => [`#/link-to/${nodeType}-logs/`, nodeName, ...(time ? [`?time=${time}`] : [])].join('');

View file

@ -1,27 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { createQueryStringForDetailTime } from './create_query_string_for_detail_time';
export const RedirectToPodDetail = ({ match, location }: RouteComponentProps<{ name: string }>) => {
const args = createQueryStringForDetailTime(location);
return <Redirect to={`/metrics/pod/${match.params.name}${args}`} />;
};
export const getPodDetailUrl = ({
name,
to,
from,
}: {
name: string;
to?: number;
from?: number;
}) => {
const args = to && from ? `?to=${to}&from=${from}` : '';
return `#/link-to/pod-detail/${name}${args}`;
};

View file

@ -1,35 +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 compose from 'lodash/fp/compose';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { LoadingPage } from '../../components/loading_page';
import { replaceLogFilterInQueryString } from '../../containers/logs/with_log_filter';
import { replaceLogPositionInQueryString } from '../../containers/logs/with_log_position';
import { WithSource } from '../../containers/with_source';
import { getTimeFromLocation } from './query_params';
export const RedirectToPodLogs = ({ match, location }: RouteComponentProps<{ podId: string }>) => (
<WithSource>
{({ configuredFields }) => {
if (!configuredFields) {
return <LoadingPage message="Loading pod logs" />;
}
const searchString = compose(
replaceLogFilterInQueryString(`${configuredFields.pod}: ${match.params.podId}`),
replaceLogPositionInQueryString(getTimeFromLocation(location))
)('');
return <Redirect to={`/logs?${searchString}`} />;
}}
</WithSource>
);
export const getPodLogsUrl = ({ podId, time }: { podId: string; time?: number }) =>
['#/link-to/pod-logs/', podId, ...(time ? [`?time=${time}`] : [])].join('');

View file

@ -17,7 +17,7 @@ export const selectTimeUpdatePolicyInterval = (state: WaffleTimeState) =>
state.updatePolicy.policy === 'interval' ? state.updatePolicy.interval : null;
export const selectCurrentTimeRange = createSelector(selectCurrentTime, currentTime => ({
from: currentTime - 1000 * 60 * 10,
interval: '5m',
from: currentTime - 1000 * 60 * 60,
interval: '1m',
to: currentTime,
}));