[Metrics UI] Refactor With* containers to hooks (#59503)

* [Metrics UI] Refactor containers to hooks

* clean up depends; move useInterval out of useWaffleTime;

* converting WithWaffleFilters to useWaffleFilters

* Removing WithWaffleOptions

* Refactor WithWaffleViewState to useWaffleViewState

* Removing obsolete files

* Fixing race condition with complext state

* Adding undefined to RisonValue; unwinding changes trying to work around bad type

* Switching to context

* Change assertion to ignore the length of the current URL

* Fixing test frameork to accept urls longer then 230 characters

* Fixes #59395; Refactor WithMetricsTime to hook; Fixes brushing on metric detail page; fixes refresh button on metric detail page

* Fixing tests with adding timeRange

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Chris Cowan 2020-04-14 09:01:41 -07:00 committed by GitHub
parent d015c24509
commit c2f2a79acb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 1086 additions and 2148 deletions

View file

@ -247,25 +247,11 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
}
currentUrl = (await browser.getCurrentUrl()).replace(/\/\/\w+:\w+@/, '//');
const maxAdditionalLengthOnNavUrl = 230;
// On several test failures at the end of the TileMap test we try to navigate back to
// Visualize so we can create the next Vertical Bar Chart, but we can see from the
// logging and the screenshot that it's still on the TileMap page. Why didn't the "get"
// with a new timestamped URL go? I thought that sleep(700) between the get and the
// refresh would solve the problem but didn't seem to always work.
// So this hack fails the navSuccessful check if the currentUrl doesn't match the
// appUrl plus up to 230 other chars.
// Navigating to Settings when there is a default index pattern has a URL length of 196
// (from debug output). Some other tabs may also be long. But a rather simple configured
// visualization is about 1000 chars long. So at least we catch that case.
// Browsers don't show the ':port' if it's 80 or 443 so we have to
// remove that part so we can get a match in the tests.
const navSuccessful = new RegExp(
appUrl.replace(':80/', '/').replace(':443/', '/') +
`.{0,${maxAdditionalLengthOnNavUrl}}$`
).test(currentUrl);
const navSuccessful = currentUrl
.replace(':80/', '/')
.replace(':443/', '/')
.startsWith(appUrl);
if (!navSuccessful) {
const msg = `App failed to load: ${appName} in ${defaultFindTimeout}ms appUrl=${appUrl} currentUrl=${currentUrl}`;

View file

@ -18,7 +18,7 @@
*/
declare module 'rison-node' {
export type RisonValue = null | boolean | number | string | RisonObject | RisonArray;
export type RisonValue = undefined | null | boolean | number | string | RisonObject | RisonArray;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface RisonArray extends Array<RisonValue> {}

View file

@ -18,7 +18,7 @@
*/
declare module 'rison-node' {
export type RisonValue = null | boolean | number | string | RisonObject | RisonArray;
export type RisonValue = undefined | null | boolean | number | string | RisonObject | RisonArray;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface RisonArray extends Array<RisonValue> {}

View file

@ -11,6 +11,10 @@ export const InfraMetadataRequestRT = rt.type({
nodeId: rt.string,
nodeType: ItemTypeRT,
sourceId: rt.string,
timeRange: rt.type({
from: rt.number,
to: rt.number,
}),
});
export const InfraMetadataFeatureRT = rt.type({

View file

@ -20,7 +20,7 @@ import { withTheme } from '../../../../observability/public';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details';
export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
export const Layout = withTheme(({ metrics, theme, onChangeRangeTime }: LayoutPropsWithTheme) => (
<React.Fragment>
<MetadataDetails
fields={[
@ -42,6 +42,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
}
)}
metrics={metrics}
onChangeRangeTime={onChangeRangeTime}
>
<SubSection
id="awsEC2CpuUtilization"

View file

@ -18,7 +18,7 @@ import { withTheme } from '../../../../observability/public';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { LayoutContent } from '../../../public/pages/metrics/components/layout_content';
export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => (
<React.Fragment>
<LayoutContent>
<Section
@ -30,6 +30,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
}
)}
metrics={metrics}
onChangeRangeTime={onChangeRangeTime}
>
<SubSection
id="awsRDSCpuTotal"

View file

@ -18,7 +18,7 @@ import { withTheme } from '../../../../observability/public';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { LayoutContent } from '../../../public/pages/metrics/components/layout_content';
export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => (
<React.Fragment>
<LayoutContent>
<Section
@ -30,6 +30,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
}
)}
metrics={metrics}
onChangeRangeTime={onChangeRangeTime}
>
<SubSection
id="awsS3BucketSize"

View file

@ -18,7 +18,7 @@ import { withTheme } from '../../../../observability/public';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { LayoutContent } from '../../../public/pages/metrics/components/layout_content';
export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => (
<React.Fragment>
<LayoutContent>
<Section
@ -30,6 +30,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
}
)}
metrics={metrics}
onChangeRangeTime={onChangeRangeTime}
>
<SubSection
id="awsSQSMessagesVisible"

View file

@ -22,7 +22,7 @@ import { LayoutContent } from '../../../public/pages/metrics/components/layout_c
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details';
export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => (
<React.Fragment>
<MetadataDetails />
<LayoutContent>
@ -40,6 +40,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
}
)}
metrics={metrics}
onChangeRangeTime={onChangeRangeTime}
>
<SubSection id="containerOverview">
<GaugesSectionVis

View file

@ -24,7 +24,7 @@ import { MetadataDetails } from '../../../public/pages/metrics/components/metada
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { LayoutContent } from '../../../public/pages/metrics/components/layout_content';
export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => (
<React.Fragment>
<MetadataDetails
fields={[
@ -52,6 +52,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
}
)}
metrics={metrics}
onChangeRangeTime={onChangeRangeTime}
>
<SubSection id="hostSystemOverview">
<GaugesSectionVis
@ -242,6 +243,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
}
)}
metrics={metrics}
onChangeRangeTime={onChangeRangeTime}
>
<SubSection id="hostK8sOverview">
<GaugesSectionVis
@ -371,8 +373,8 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
/>
</SubSection>
</Section>
<Aws.Layout metrics={metrics} />
<Ngnix.Layout metrics={metrics} />
<Aws.Layout metrics={metrics} onChangeRangeTime={onChangeRangeTime} />
<Ngnix.Layout metrics={metrics} onChangeRangeTime={onChangeRangeTime} />
</LayoutContent>
</React.Fragment>
));

View file

@ -23,7 +23,7 @@ import { MetadataDetails } from '../../../public/pages/metrics/components/metada
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { LayoutContent } from '../../../public/pages/metrics/components/layout_content';
export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => (
<React.Fragment>
<MetadataDetails />
<LayoutContent>
@ -38,6 +38,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
}
)}
metrics={metrics}
onChangeRangeTime={onChangeRangeTime}
>
<SubSection id="podOverview">
<GaugesSectionVis
@ -161,7 +162,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
/>
</SubSection>
</Section>
<Nginx.Layout metrics={metrics} />
<Nginx.Layout metrics={metrics} onChangeRangeTime={onChangeRangeTime} />
</LayoutContent>
</React.Fragment>
));

View file

@ -18,7 +18,7 @@ import { ChartSectionVis } from '../../../../public/pages/metrics/components/cha
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { withTheme } from '../../../../../observability/public';
export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => (
<React.Fragment>
<Section
navLabel="AWS"
@ -29,6 +29,7 @@ export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
}
)}
metrics={metrics}
onChangeRangeTime={onChangeRangeTime}
>
<SubSection id="awsOverview">
<GaugesSectionVis

View file

@ -16,9 +16,14 @@ import { ChartSectionVis } from '../../../../public/pages/metrics/components/cha
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { withTheme } from '../../../../../observability/public';
export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => (
export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => (
<React.Fragment>
<Section navLabel="Nginx" sectionLabel="Nginx" metrics={metrics}>
<Section
navLabel="Nginx"
sectionLabel="Nginx"
metrics={metrics}
onChangeRangeTime={onChangeRangeTime}
>
<SubSection
id="nginxHits"
label={i18n.translate(

View file

@ -7,7 +7,7 @@
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ElasticsearchMappingOf } from '../../server/utils/typed_elasticsearch_mappings';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { WaffleViewState } from '../../public/containers/waffle/with_waffle_view_state';
import { WaffleViewState } from '../../public/pages/inventory_view/hooks/use_waffle_view_state';
export const inventoryViewSavedObjectType = 'inventory-view';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
@ -102,7 +102,7 @@ export const inventoryViewSavedObjectMappings: {
type: 'boolean',
},
time: {
type: 'integer',
type: 'long',
},
autoReload: {
type: 'boolean',
@ -117,6 +117,12 @@ export const inventoryViewSavedObjectMappings: {
},
},
},
accountId: {
type: 'keyword',
},
region: {
type: 'keyword',
},
},
},
};

View file

@ -8,9 +8,6 @@ import { createBrowserHistory } from 'history';
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { BehaviorSubject } from 'rxjs';
import { pluck } from 'rxjs/operators';
import { CoreStart, AppMountParameters } from 'kibana/public';
// TODO use theme provided from parentApp when kibana supports it
@ -18,9 +15,7 @@ import { EuiErrorBoundary } from '@elastic/eui';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { EuiThemeProvider } from '../../../observability/public/typings/eui_styled_components';
import { InfraFrontendLibs } from '../lib/lib';
import { createStore } from '../store';
import { ApolloClientContext } from '../utils/apollo_context';
import { ReduxStateContextProvider } from '../utils/redux_context';
import { HistoryContext } from '../utils/history_context';
import {
useUiSetting$,
@ -43,12 +38,6 @@ export async function startApp(
) {
const { element, appBasePath } = params;
const history = createBrowserHistory({ basename: appBasePath });
const libs$ = new BehaviorSubject(libs);
const store = createStore({
apolloClient: libs$.pipe(pluck('apolloClient')),
observableApi: libs$.pipe(pluck('observableApi')),
});
const InfraPluginRoot: React.FunctionComponent = () => {
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
@ -56,19 +45,15 @@ export async function startApp(
<core.i18n.Context>
<EuiErrorBoundary>
<TriggersActionsProvider triggersActionsUI={triggersActionsUI}>
<ReduxStoreProvider store={store}>
<ReduxStateContextProvider>
<ApolloProvider client={libs.apolloClient}>
<ApolloClientContext.Provider value={libs.apolloClient}>
<EuiThemeProvider darkMode={darkMode}>
<HistoryContext.Provider value={history}>
<Router history={history} />
</HistoryContext.Provider>
</EuiThemeProvider>
</ApolloClientContext.Provider>
</ApolloProvider>
</ReduxStateContextProvider>
</ReduxStoreProvider>
<ApolloProvider client={libs.apolloClient}>
<ApolloClientContext.Provider value={libs.apolloClient}>
<EuiThemeProvider darkMode={darkMode}>
<HistoryContext.Provider value={history}>
<Router history={history} />
</HistoryContext.Provider>
</EuiThemeProvider>
</ApolloClientContext.Provider>
</ApolloProvider>
</TriggersActionsProvider>
</EuiErrorBoundary>
</core.i18n.Context>

View file

@ -5,64 +5,89 @@
*/
import React from 'react';
import { InfraWaffleMapOptions, InfraWaffleMapBounds } from '../../lib/lib';
import { KueryFilterQuery } from '../../store/local/waffle_filter';
import { useInterval } from 'react-use';
import { euiPaletteColorBlind } from '@elastic/eui';
import { NodesOverview } from '../nodes_overview';
import { Toolbar } from './toolbars/toolbar';
import { PageContent } from '../page';
import { useSnapshot } from '../../containers/waffle/use_snaphot';
import { useInventoryMeta } from '../../containers/inventory_metadata/use_inventory_meta';
import { SnapshotMetricInput, SnapshotGroupBy } from '../../../common/http_api/snapshot_api';
import { InventoryItemType } from '../../../common/inventory_models/types';
import { useWaffleTimeContext } from '../../pages/inventory_view/hooks/use_waffle_time';
import { useWaffleFiltersContext } from '../../pages/inventory_view/hooks/use_waffle_filters';
import { useWaffleOptionsContext } from '../../pages/inventory_view/hooks/use_waffle_options';
import { useSourceContext } from '../../containers/source';
import { InfraFormatterType, InfraWaffleMapGradientLegend } from '../../lib/lib';
export interface LayoutProps {
options: InfraWaffleMapOptions;
nodeType: InventoryItemType;
onDrilldown: (filter: KueryFilterQuery) => void;
currentTime: number;
onViewChange: (view: string) => void;
view: string;
boundsOverride: InfraWaffleMapBounds;
autoBounds: boolean;
const euiVisColorPalette = euiPaletteColorBlind();
filterQuery: string | null | undefined;
metric: SnapshotMetricInput;
groupBy: SnapshotGroupBy;
sourceId: string;
accountId: string;
region: string;
}
export const Layout = (props: LayoutProps) => {
const { accounts, regions } = useInventoryMeta(props.sourceId, props.nodeType);
export const Layout = () => {
const { sourceId, source } = useSourceContext();
const {
metric,
groupBy,
nodeType,
accountId,
region,
changeView,
view,
autoBounds,
boundsOverride,
} = useWaffleOptionsContext();
const { accounts, regions } = useInventoryMeta(sourceId, nodeType);
const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext();
const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext();
const { loading, nodes, reload, interval } = useSnapshot(
props.filterQuery,
props.metric,
props.groupBy,
props.nodeType,
props.sourceId,
props.currentTime,
props.accountId,
props.region
filterQueryAsJson,
metric,
groupBy,
nodeType,
sourceId,
currentTime,
accountId,
region
);
const options = {
formatter: InfraFormatterType.percent,
formatTemplate: '{{value}}',
legend: {
type: 'gradient',
rules: [
{ value: 0, color: '#D3DAE6' },
{ value: 1, color: euiVisColorPalette[1] },
],
} as InfraWaffleMapGradientLegend,
metric,
fields: source?.configuration?.fields,
groupBy,
};
useInterval(
() => {
if (!loading) {
jumpToTime(Date.now());
}
},
isAutoReloading ? 5000 : null
);
return (
<>
<Toolbar accounts={accounts} regions={regions} nodeType={props.nodeType} />
<Toolbar accounts={accounts} regions={regions} nodeType={nodeType} />
<PageContent>
<NodesOverview
nodes={nodes}
options={props.options}
nodeType={props.nodeType}
options={options}
nodeType={nodeType}
loading={loading}
reload={reload}
onDrilldown={props.onDrilldown}
currentTime={props.currentTime}
onViewChange={props.onViewChange}
view={props.view}
autoBounds={props.autoBounds}
boundsOverride={props.boundsOverride}
onDrilldown={applyFilterQuery}
currentTime={currentTime}
onViewChange={changeView}
view={view}
autoBounds={autoBounds}
boundsOverride={boundsOverride}
interval={interval}
/>
</PageContent>

View file

@ -0,0 +1,21 @@
/*
* 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 { SavedViewsToolbarControls } from '../../saved_views/toolbar_control';
import { inventoryViewSavedObjectType } from '../../../../common/saved_objects/inventory_view';
import { useWaffleViewState } from '../../../pages/inventory_view/hooks/use_waffle_view_state';
export const SavedViews = () => {
const { viewState, defaultViewState, onViewChange } = useWaffleViewState();
return (
<SavedViewsToolbarControls
defaultViewState={defaultViewState}
viewState={viewState}
onViewChange={onViewChange}
viewType={inventoryViewSavedObjectType}
/>
);
};

View file

@ -5,7 +5,6 @@
*/
import React, { FunctionComponent } from 'react';
import { Action } from 'typescript-fsa';
import { EuiFlexItem } from '@elastic/eui';
import {
SnapshotMetricInput,
@ -16,33 +15,23 @@ import { InventoryCloudAccount } from '../../../../common/http_api/inventory_met
import { findToolbar } from '../../../../common/inventory_models/toolbars';
import { ToolbarWrapper } from './toolbar_wrapper';
import { waffleOptionsSelectors } from '../../../store';
import { InfraGroupByOptions } from '../../../lib/lib';
import { WithWaffleViewState } from '../../../containers/waffle/with_waffle_view_state';
import { SavedViewsToolbarControls } from '../../saved_views/toolbar_control';
import { inventoryViewSavedObjectType } from '../../../../common/saved_objects/inventory_view';
import { IIndexPattern } from '../../../../../../../src/plugins/data/public';
import { InventoryItemType } from '../../../../common/inventory_models/types';
import { WaffleOptionsState } from '../../../pages/inventory_view/hooks/use_waffle_options';
import { SavedViews } from './save_views';
export interface ToolbarProps {
export interface ToolbarProps
extends Omit<WaffleOptionsState, 'view' | 'boundsOverride' | 'autoBounds'> {
createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern;
changeMetric: (payload: SnapshotMetricInput) => Action<SnapshotMetricInput>;
changeGroupBy: (payload: SnapshotGroupBy) => Action<SnapshotGroupBy>;
changeCustomOptions: (payload: InfraGroupByOptions[]) => Action<InfraGroupByOptions[]>;
changeAccount: (id: string) => Action<string>;
changeRegion: (name: string) => Action<string>;
customOptions: ReturnType<typeof waffleOptionsSelectors.selectCustomOptions>;
groupBy: ReturnType<typeof waffleOptionsSelectors.selectGroupBy>;
metric: ReturnType<typeof waffleOptionsSelectors.selectMetric>;
nodeType: ReturnType<typeof waffleOptionsSelectors.selectNodeType>;
accountId: ReturnType<typeof waffleOptionsSelectors.selectAccountId>;
region: ReturnType<typeof waffleOptionsSelectors.selectRegion>;
changeMetric: (payload: SnapshotMetricInput) => void;
changeGroupBy: (payload: SnapshotGroupBy) => void;
changeCustomOptions: (payload: InfraGroupByOptions[]) => void;
changeAccount: (id: string) => void;
changeRegion: (name: string) => void;
accounts: InventoryCloudAccount[];
regions: string[];
customMetrics: ReturnType<typeof waffleOptionsSelectors.selectCustomMetrics>;
changeCustomMetrics: (
payload: SnapshotCustomMetricInput[]
) => Action<SnapshotCustomMetricInput[]>;
changeCustomMetrics: (payload: SnapshotCustomMetricInput[]) => void;
}
const wrapToolbarItems = (
@ -57,16 +46,7 @@ const wrapToolbarItems = (
<ToolbarItems {...props} accounts={accounts} regions={regions} />
<EuiFlexItem grow={true} />
<EuiFlexItem grow={false}>
<WithWaffleViewState indexPattern={props.createDerivedIndexPattern('metrics')}>
{({ defaultViewState, viewState, onViewChange }) => (
<SavedViewsToolbarControls
defaultViewState={defaultViewState}
viewState={viewState}
onViewChange={onViewChange}
viewType={inventoryViewSavedObjectType}
/>
)}
</WithWaffleViewState>
<SavedViews />
</EuiFlexItem>
</>
)}

View file

@ -8,58 +8,52 @@ import React from 'react';
import { EuiFlexGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SnapshotMetricType } from '../../../../common/inventory_models/types';
import { WithSource } from '../../../containers/with_source';
import { WithWaffleOptions } from '../../../containers/waffle/with_waffle_options';
import { Toolbar } from '../../eui/toolbar';
import { ToolbarProps } from './toolbar';
import { fieldToName } from '../../waffle/lib/field_to_display_name';
import { useSourceContext } from '../../../containers/source';
import { useWaffleOptionsContext } from '../../../pages/inventory_view/hooks/use_waffle_options';
interface Props {
children: (props: Omit<ToolbarProps, 'accounts' | 'regions'>) => React.ReactElement;
}
export const ToolbarWrapper = (props: Props) => {
const {
changeMetric,
changeGroupBy,
changeCustomOptions,
changeAccount,
changeRegion,
customOptions,
groupBy,
metric,
nodeType,
accountId,
region,
customMetrics,
changeCustomMetrics,
} = useWaffleOptionsContext();
const { createDerivedIndexPattern } = useSourceContext();
return (
<Toolbar>
<EuiFlexGroup alignItems="center" gutterSize="m">
<WithSource>
{({ createDerivedIndexPattern }) => (
<WithWaffleOptions>
{({
changeMetric,
changeGroupBy,
changeCustomOptions,
changeAccount,
changeRegion,
customOptions,
groupBy,
metric,
nodeType,
accountId,
region,
customMetrics,
changeCustomMetrics,
}) =>
props.children({
createDerivedIndexPattern,
changeMetric,
changeGroupBy,
changeAccount,
changeRegion,
changeCustomOptions,
customOptions,
groupBy,
metric,
nodeType,
region,
accountId,
customMetrics,
changeCustomMetrics,
})
}
</WithWaffleOptions>
)}
</WithSource>
{props.children({
createDerivedIndexPattern,
changeMetric,
changeGroupBy,
changeAccount,
changeRegion,
changeCustomOptions,
customOptions,
groupBy,
metric,
nodeType,
region,
accountId,
customMetrics,
changeCustomMetrics,
})}
</EuiFlexGroup>
</Toolbar>
);

View file

@ -12,7 +12,6 @@ import React from 'react';
import { euiStyled } from '../../../../observability/public';
import { InfraFormatterType, InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib';
import { KueryFilterQuery } from '../../store/local/waffle_filter';
import { createFormatter } from '../../utils/formatters';
import { NoData } from '../empty_states';
import { InfraLoadingPanel } from '../loading';
@ -24,6 +23,11 @@ import { convertIntervalToString } from '../../utils/convert_interval_to_string'
import { InventoryItemType } from '../../../common/inventory_models/types';
import { createFormatterForMetric } from '../metrics_explorer/helpers/create_formatter_for_metric';
export interface KueryFilterQuery {
kind: 'kuery';
expression: string;
}
interface Props {
options: InfraWaffleMapOptions;
nodeType: InventoryItemType;

View file

@ -6,12 +6,12 @@
import React from 'react';
import { euiStyled } from '../../../../observability/public';
import { WithWaffleOptions } from '../../containers/waffle/with_waffle_options';
import { InfraFormatter, InfraWaffleMapBounds, InfraWaffleMapLegend } from '../../lib/lib';
import { GradientLegend } from './gradient_legend';
import { LegendControls } from './legend_controls';
import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from './lib/type_guards';
import { StepLegend } from './steps_legend';
import { useWaffleOptionsContext } from '../../pages/inventory_view/hooks/use_waffle_options';
interface Props {
legend: InfraWaffleMapLegend;
bounds: InfraWaffleMapBounds;
@ -25,22 +25,24 @@ interface LegendControlOptions {
}
export const Legend: React.FC<Props> = ({ dataBounds, legend, bounds, formatter }) => {
const {
changeBoundsOverride,
changeAutoBounds,
autoBounds,
boundsOverride,
} = useWaffleOptionsContext();
return (
<LegendContainer>
<WithWaffleOptions>
{({ changeBoundsOverride, changeAutoBounds, autoBounds, boundsOverride }) => (
<LegendControls
dataBounds={dataBounds}
bounds={bounds}
autoBounds={autoBounds}
boundsOverride={boundsOverride}
onChange={(options: LegendControlOptions) => {
changeBoundsOverride(options.bounds);
changeAutoBounds(options.auto);
}}
/>
)}
</WithWaffleOptions>
<LegendControls
dataBounds={dataBounds}
bounds={bounds}
autoBounds={autoBounds}
boundsOverride={boundsOverride}
onChange={(options: LegendControlOptions) => {
changeBoundsOverride(options.bounds);
changeAutoBounds(options.auto);
}}
/>
{isInfraWaffleMapGradientLegend(legend) && (
<GradientLegend formatter={formatter} legend={legend} bounds={bounds} />
)}

View file

@ -5,11 +5,7 @@
*/
import { createUptimeLink } from './create_uptime_link';
import {
InfraWaffleMapOptions,
InfraWaffleMapLegendMode,
InfraFormatterType,
} from '../../../lib/lib';
import { InfraWaffleMapOptions, InfraFormatterType } from '../../../lib/lib';
import { SnapshotMetricType } from '../../../../common/inventory_models/types';
const options: InfraWaffleMapOptions = {
@ -26,7 +22,7 @@ const options: InfraWaffleMapOptions = {
metric: { type: 'cpu' },
groupBy: [],
legend: {
type: InfraWaffleMapLegendMode.gradient,
type: 'gradient',
rules: [],
},
};

View file

@ -4,16 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
InfraWaffleMapGradientLegend,
InfraWaffleMapLegendMode,
InfraWaffleMapStepLegend,
} from '../../../lib/lib';
import { InfraWaffleMapGradientLegend, InfraWaffleMapStepLegend } from '../../../lib/lib';
export function isInfraWaffleMapStepLegend(subject: any): subject is InfraWaffleMapStepLegend {
return subject.type && subject.type === InfraWaffleMapLegendMode.step;
return subject.type && subject.type === 'step';
}
export function isInfraWaffleMapGradientLegend(
subject: any
): subject is InfraWaffleMapGradientLegend {
return subject.type && subject.type === InfraWaffleMapLegendMode.gradient;
return subject.type && subject.type === 'gradient';
}

View file

@ -16,36 +16,23 @@ import React, { useCallback, useState, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { findInventoryModel } from '../../../common/inventory_models';
import { InventoryItemType } from '../../../common/inventory_models/types';
import {
SnapshotMetricInput,
SnapshotGroupBy,
SnapshotCustomMetricInput,
} from '../../../common/http_api/snapshot_api';
interface WaffleInventorySwitcherProps {
nodeType: InventoryItemType;
changeNodeType: (nodeType: InventoryItemType) => void;
changeGroupBy: (groupBy: SnapshotGroupBy) => void;
changeMetric: (metric: SnapshotMetricInput) => void;
changeCustomMetrics: (metrics: SnapshotCustomMetricInput[]) => void;
changeAccount: (id: string) => void;
changeRegion: (name: string) => void;
}
import { useWaffleOptionsContext } from '../../pages/inventory_view/hooks/use_waffle_options';
const getDisplayNameForType = (type: InventoryItemType) => {
const inventoryModel = findInventoryModel(type);
return inventoryModel.displayName;
};
export const WaffleInventorySwitcher: React.FC<WaffleInventorySwitcherProps> = ({
changeNodeType,
changeGroupBy,
changeMetric,
changeAccount,
changeRegion,
changeCustomMetrics,
nodeType,
}) => {
export const WaffleInventorySwitcher: React.FC = () => {
const {
changeNodeType,
changeGroupBy,
changeMetric,
changeAccount,
changeRegion,
changeCustomMetrics,
nodeType,
} = useWaffleOptionsContext();
const [isOpen, setIsOpen] = useState(false);
const closePopover = useCallback(() => setIsOpen(false), []);
const openPopover = useCallback(() => setIsOpen(true), []);

View file

@ -7,84 +7,60 @@
import { EuiButtonEmpty, EuiDatePicker, EuiFormControlLayout } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import moment, { Moment } from 'moment';
import React from 'react';
import { Action } from 'typescript-fsa';
import React, { useCallback } from 'react';
import { useWaffleTimeContext } from '../../pages/inventory_view/hooks/use_waffle_time';
interface WaffleTimeControlsProps {
currentTime: number;
isLiveStreaming?: boolean;
onChangeTime?: (time: number) => void;
startLiveStreaming?: (payload: void) => Action<void>;
stopLiveStreaming?: (payload: void) => Action<void>;
}
export const WaffleTimeControls = () => {
const {
currentTime,
isAutoReloading,
startAutoReload,
stopAutoReload,
jumpToTime,
} = useWaffleTimeContext();
export class WaffleTimeControls extends React.Component<WaffleTimeControlsProps> {
public render() {
const { currentTime, isLiveStreaming } = this.props;
const currentMoment = moment(currentTime);
const currentMoment = moment(currentTime);
const liveStreamingButton = isAutoReloading ? (
<EuiButtonEmpty color="primary" iconSide="left" iconType="pause" onClick={stopAutoReload}>
<FormattedMessage
id="xpack.infra.waffleTime.stopRefreshingButtonLabel"
defaultMessage="Stop refreshing"
/>
</EuiButtonEmpty>
) : (
<EuiButtonEmpty iconSide="left" iconType="play" onClick={startAutoReload}>
<FormattedMessage
id="xpack.infra.waffleTime.autoRefreshButtonLabel"
defaultMessage="Auto-refresh"
/>
</EuiButtonEmpty>
);
const liveStreamingButton = isLiveStreaming ? (
<EuiButtonEmpty
color="primary"
iconSide="left"
iconType="pause"
onClick={this.stopLiveStreaming}
>
<FormattedMessage
id="xpack.infra.waffleTime.stopRefreshingButtonLabel"
defaultMessage="Stop refreshing"
/>
</EuiButtonEmpty>
) : (
<EuiButtonEmpty iconSide="left" iconType="play" onClick={this.startLiveStreaming}>
<FormattedMessage
id="xpack.infra.waffleTime.autoRefreshButtonLabel"
defaultMessage="Auto-refresh"
/>
</EuiButtonEmpty>
);
const handleChangeDate = useCallback(
(time: Moment | null) => {
if (time) {
jumpToTime(time.valueOf());
}
},
[jumpToTime]
);
return (
<EuiFormControlLayout append={liveStreamingButton} data-test-subj="waffleDatePicker">
<EuiDatePicker
className="euiFieldText--inGroup"
dateFormat="L LTS"
disabled={isLiveStreaming}
injectTimes={currentMoment ? [currentMoment] : []}
isLoading={isLiveStreaming}
onChange={this.handleChangeDate}
popperPlacement="top-end"
selected={currentMoment}
shouldCloseOnSelect
showTimeSelect
timeFormat="LT"
/>
</EuiFormControlLayout>
);
}
private handleChangeDate = (time: Moment | null) => {
const { onChangeTime } = this.props;
if (onChangeTime && time) {
onChangeTime(time.valueOf());
}
};
private startLiveStreaming = () => {
const { startLiveStreaming } = this.props;
if (startLiveStreaming) {
startLiveStreaming();
}
};
private stopLiveStreaming = () => {
const { stopLiveStreaming } = this.props;
if (stopLiveStreaming) {
stopLiveStreaming();
}
};
}
return (
<EuiFormControlLayout append={liveStreamingButton} data-test-subj="waffleDatePicker">
<EuiDatePicker
className="euiFieldText--inGroup"
dateFormat="L LTS"
disabled={isAutoReloading}
injectTimes={currentMoment ? [currentMoment] : []}
isLoading={isAutoReloading}
onChange={handleChangeDate}
popperPlacement="top-end"
selected={currentMoment}
shouldCloseOnSelect
showTimeSelect
timeFormat="LT"
/>
</EuiFormControlLayout>
);
};

View file

@ -13,17 +13,18 @@ import { useHTTPRequest } from '../../hooks/use_http_request';
import { throwErrors, createPlainError } from '../../../common/runtime_types';
import { InventoryMetric, InventoryItemType } from '../../../common/inventory_models/types';
import { getFilteredMetrics } from './lib/get_filtered_metrics';
import { MetricsTimeInput } from '../../pages/metrics/hooks/use_metrics_time';
export function useMetadata(
nodeId: string,
nodeType: InventoryItemType,
requiredMetrics: InventoryMetric[],
sourceId: string
sourceId: string,
timeRange: MetricsTimeInput
) {
const decodeResponse = (response: any) => {
return pipe(InfraMetadataRT.decode(response), fold(throwErrors(createPlainError), identity));
};
const { error, loading, response, makeRequest } = useHTTPRequest<InfraMetadata>(
'/api/infra/metadata',
'POST',
@ -31,6 +32,7 @@ export function useMetadata(
nodeId,
nodeType,
sourceId,
timeRange: { from: timeRange.from, to: timeRange.to },
}),
decodeResponse
);

View file

@ -1,7 +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.
*/
export * from './with_waffle_filters';

View file

@ -1,37 +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 gql from 'graphql-tag';
export const waffleNodesQuery = gql`
query WaffleNodesQuery(
$sourceId: ID!
$timerange: InfraTimerangeInput!
$filterQuery: String
$metric: InfraSnapshotMetricInput!
$groupBy: [InfraSnapshotGroupbyInput!]!
$type: InfraNodeType!
) {
source(id: $sourceId) {
id
snapshot(timerange: $timerange, filterQuery: $filterQuery) {
nodes(groupBy: $groupBy, metric: $metric, type: $type) {
path {
value
label
ip
}
metric {
name
value
avg
max
}
}
}
}
}
`;

View file

@ -1,96 +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 { connect } from 'react-redux';
import { IIndexPattern } from 'src/plugins/data/public';
import { State, waffleFilterActions, waffleFilterSelectors } from '../../store';
import { FilterQuery } from '../../store/local/waffle_filter';
import { convertKueryToElasticSearchQuery } from '../../utils/kuery';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { UrlStateContainer } from '../../utils/url_state';
interface WithWaffleFilterProps {
indexPattern: IIndexPattern;
}
export const withWaffleFilter = connect(
(state: State) => ({
filterQuery: waffleFilterSelectors.selectWaffleFilterQuery(state),
filterQueryDraft: waffleFilterSelectors.selectWaffleFilterQueryDraft(state),
filterQueryAsJson: waffleFilterSelectors.selectWaffleFilterQueryAsJson(state),
isFilterQueryDraftValid: waffleFilterSelectors.selectIsWaffleFilterQueryDraftValid(state),
}),
(dispatch, ownProps: WithWaffleFilterProps) =>
bindPlainActionCreators({
applyFilterQuery: (query: FilterQuery) =>
waffleFilterActions.applyWaffleFilterQuery({
query,
serializedQuery: convertKueryToElasticSearchQuery(
query.expression,
ownProps.indexPattern
),
}),
applyFilterQueryFromKueryExpression: (expression: string) =>
waffleFilterActions.applyWaffleFilterQuery({
query: {
kind: 'kuery',
expression,
},
serializedQuery: convertKueryToElasticSearchQuery(expression, ownProps.indexPattern),
}),
setFilterQueryDraft: waffleFilterActions.setWaffleFilterQueryDraft,
setFilterQueryDraftFromKueryExpression: (expression: string) =>
waffleFilterActions.setWaffleFilterQueryDraft({
kind: 'kuery',
expression,
}),
})
);
export const WithWaffleFilter = asChildFunctionRenderer(withWaffleFilter);
/**
* Url State
*/
type WaffleFilterUrlState = ReturnType<typeof waffleFilterSelectors.selectWaffleFilterQuery>;
type WithWaffleFilterUrlStateProps = WithWaffleFilterProps;
export const WithWaffleFilterUrlState: React.FC<WithWaffleFilterUrlStateProps> = ({
indexPattern,
}) => (
<WithWaffleFilter indexPattern={indexPattern}>
{({ applyFilterQuery, filterQuery }) => (
<UrlStateContainer
urlState={filterQuery}
urlStateKey="waffleFilter"
mapToUrlState={mapToUrlState}
onChange={urlState => {
if (urlState) {
applyFilterQuery(urlState);
}
}}
onInitialize={urlState => {
if (urlState) {
applyFilterQuery(urlState);
}
}}
/>
)}
</WithWaffleFilter>
);
const mapToUrlState = (value: any): WaffleFilterUrlState | undefined =>
value && value.kind === 'kuery' && typeof value.expression === 'string'
? {
kind: value.kind,
expression: value.expression,
}
: undefined;

View file

@ -1,265 +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 { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { isBoolean, isNumber } from 'lodash';
import { InfraGroupByOptions } from '../../lib/lib';
import { State, waffleOptionsActions, waffleOptionsSelectors } from '../../store';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { UrlStateContainer } from '../../utils/url_state';
import {
SnapshotMetricInput,
SnapshotGroupBy,
SnapshotCustomMetricInputRT,
} from '../../../common/http_api/snapshot_api';
import {
SnapshotMetricTypeRT,
InventoryItemType,
ItemTypeRT,
} from '../../../common/inventory_models/types';
const selectOptionsUrlState = createSelector(
waffleOptionsSelectors.selectMetric,
waffleOptionsSelectors.selectView,
waffleOptionsSelectors.selectGroupBy,
waffleOptionsSelectors.selectNodeType,
waffleOptionsSelectors.selectCustomOptions,
waffleOptionsSelectors.selectBoundsOverride,
waffleOptionsSelectors.selectAutoBounds,
waffleOptionsSelectors.selectAccountId,
waffleOptionsSelectors.selectRegion,
waffleOptionsSelectors.selectCustomMetrics,
(
metric,
view,
groupBy,
nodeType,
customOptions,
boundsOverride,
autoBounds,
accountId,
region,
customMetrics
) => ({
metric,
groupBy,
nodeType,
view,
customOptions,
boundsOverride,
autoBounds,
accountId,
region,
customMetrics,
})
);
export const withWaffleOptions = connect(
(state: State) => ({
metric: waffleOptionsSelectors.selectMetric(state),
groupBy: waffleOptionsSelectors.selectGroupBy(state),
nodeType: waffleOptionsSelectors.selectNodeType(state),
view: waffleOptionsSelectors.selectView(state),
customOptions: waffleOptionsSelectors.selectCustomOptions(state),
boundsOverride: waffleOptionsSelectors.selectBoundsOverride(state),
autoBounds: waffleOptionsSelectors.selectAutoBounds(state),
accountId: waffleOptionsSelectors.selectAccountId(state),
region: waffleOptionsSelectors.selectRegion(state),
urlState: selectOptionsUrlState(state),
customMetrics: waffleOptionsSelectors.selectCustomMetrics(state),
}),
bindPlainActionCreators({
changeMetric: waffleOptionsActions.changeMetric,
changeGroupBy: waffleOptionsActions.changeGroupBy,
changeNodeType: waffleOptionsActions.changeNodeType,
changeView: waffleOptionsActions.changeView,
changeCustomOptions: waffleOptionsActions.changeCustomOptions,
changeBoundsOverride: waffleOptionsActions.changeBoundsOverride,
changeAutoBounds: waffleOptionsActions.changeAutoBounds,
changeAccount: waffleOptionsActions.changeAccount,
changeRegion: waffleOptionsActions.changeRegion,
changeCustomMetrics: waffleOptionsActions.changeCustomMetrics,
})
);
export const WithWaffleOptions = asChildFunctionRenderer(withWaffleOptions);
/**
* Url State
*/
interface WaffleOptionsUrlState {
metric?: ReturnType<typeof waffleOptionsSelectors.selectMetric>;
groupBy?: ReturnType<typeof waffleOptionsSelectors.selectGroupBy>;
nodeType?: ReturnType<typeof waffleOptionsSelectors.selectNodeType>;
view?: ReturnType<typeof waffleOptionsSelectors.selectView>;
customOptions?: ReturnType<typeof waffleOptionsSelectors.selectCustomOptions>;
bounds?: ReturnType<typeof waffleOptionsSelectors.selectBoundsOverride>;
auto?: ReturnType<typeof waffleOptionsSelectors.selectAutoBounds>;
accountId?: ReturnType<typeof waffleOptionsSelectors.selectAccountId>;
region?: ReturnType<typeof waffleOptionsSelectors.selectRegion>;
customMetrics?: ReturnType<typeof waffleOptionsSelectors.selectCustomMetrics>;
}
export const WithWaffleOptionsUrlState = () => (
<WithWaffleOptions>
{({
changeMetric,
urlState,
changeGroupBy,
changeNodeType,
changeView,
changeCustomOptions,
changeAutoBounds,
changeBoundsOverride,
changeAccount,
changeRegion,
changeCustomMetrics,
}) => (
<UrlStateContainer<WaffleOptionsUrlState>
urlState={urlState}
urlStateKey="waffleOptions"
mapToUrlState={mapToUrlState}
onChange={newUrlState => {
if (newUrlState && newUrlState.metric) {
changeMetric(newUrlState.metric);
}
if (newUrlState && newUrlState.groupBy) {
changeGroupBy(newUrlState.groupBy);
}
if (newUrlState && newUrlState.nodeType) {
changeNodeType(newUrlState.nodeType);
}
if (newUrlState && newUrlState.view) {
changeView(newUrlState.view);
}
if (newUrlState && newUrlState.customOptions) {
changeCustomOptions(newUrlState.customOptions);
}
if (newUrlState && newUrlState.bounds) {
changeBoundsOverride(newUrlState.bounds);
}
if (newUrlState && newUrlState.auto) {
changeAutoBounds(newUrlState.auto);
}
if (newUrlState && newUrlState.accountId) {
changeAccount(newUrlState.accountId);
}
if (newUrlState && newUrlState.region) {
changeRegion(newUrlState.region);
}
if (newUrlState && newUrlState.customMetrics) {
changeCustomMetrics(newUrlState.customMetrics);
}
}}
onInitialize={initialUrlState => {
if (initialUrlState && initialUrlState.metric) {
changeMetric(initialUrlState.metric);
}
if (initialUrlState && initialUrlState.groupBy) {
changeGroupBy(initialUrlState.groupBy);
}
if (initialUrlState && initialUrlState.nodeType) {
changeNodeType(initialUrlState.nodeType);
}
if (initialUrlState && initialUrlState.view) {
changeView(initialUrlState.view);
}
if (initialUrlState && initialUrlState.customOptions) {
changeCustomOptions(initialUrlState.customOptions);
}
if (initialUrlState && initialUrlState.bounds) {
changeBoundsOverride(initialUrlState.bounds);
}
if (initialUrlState && initialUrlState.auto) {
changeAutoBounds(initialUrlState.auto);
}
if (initialUrlState && initialUrlState.accountId) {
changeAccount(initialUrlState.accountId);
}
if (initialUrlState && initialUrlState.region) {
changeRegion(initialUrlState.region);
}
if (initialUrlState && initialUrlState.customMetrics) {
changeCustomMetrics(initialUrlState.customMetrics);
}
}}
/>
)}
</WithWaffleOptions>
);
const mapToUrlState = (value: any): WaffleOptionsUrlState | undefined =>
value
? {
metric: mapToMetricUrlState(value.metric),
groupBy: mapToGroupByUrlState(value.groupBy),
nodeType: mapToNodeTypeUrlState(value.nodeType),
view: mapToViewUrlState(value.view),
customOptions: mapToCustomOptionsUrlState(value.customOptions),
bounds: mapToBoundsOverideUrlState(value.boundsOverride),
auto: mapToAutoBoundsUrlState(value.autoBounds),
accountId: value.accountId,
region: value.region,
customMetrics: mapToCustomMetricsUrlState(value.customMetrics),
}
: undefined;
const isInfraNodeType = (value: any): value is InventoryItemType => value in ItemTypeRT;
const isInfraSnapshotMetricInput = (subject: any): subject is SnapshotMetricInput => {
return subject != null && subject.type in SnapshotMetricTypeRT;
};
const isInfraSnapshotGroupbyInput = (subject: any): subject is SnapshotGroupBy => {
return subject != null && subject.type != null;
};
const isInfraGroupByOption = (subject: any): subject is InfraGroupByOptions => {
return subject != null && subject.text != null && subject.field != null;
};
const mapToMetricUrlState = (subject: any) => {
return subject && isInfraSnapshotMetricInput(subject) ? subject : undefined;
};
const mapToGroupByUrlState = (subject: any) => {
return subject && Array.isArray(subject) && subject.every(isInfraSnapshotGroupbyInput)
? subject
: undefined;
};
const mapToNodeTypeUrlState = (subject: any) => {
return isInfraNodeType(subject) ? subject : undefined;
};
const mapToViewUrlState = (subject: any) => {
return subject && ['map', 'table'].includes(subject) ? subject : undefined;
};
const mapToCustomOptionsUrlState = (subject: any) => {
return subject && Array.isArray(subject) && subject.every(isInfraGroupByOption)
? subject
: undefined;
};
const mapToCustomMetricsUrlState = (subject: any) => {
return subject && Array.isArray(subject) && subject.every(s => SnapshotCustomMetricInputRT.is(s))
? subject
: [];
};
const mapToBoundsOverideUrlState = (subject: any) => {
return subject != null && isNumber(subject.max) && isNumber(subject.min) ? subject : undefined;
};
const mapToAutoBoundsUrlState = (subject: any) => {
return subject != null && isBoolean(subject) ? subject : undefined;
};

View file

@ -1,96 +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 { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { State, waffleTimeActions, waffleTimeSelectors } from '../../store';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { UrlStateContainer } from '../../utils/url_state';
export const withWaffleTime = connect(
(state: State) => ({
currentTime: waffleTimeSelectors.selectCurrentTime(state),
currentTimeRange: waffleTimeSelectors.selectCurrentTimeRange(state),
isAutoReloading: waffleTimeSelectors.selectIsAutoReloading(state),
urlState: selectTimeUrlState(state),
}),
bindPlainActionCreators({
jumpToTime: waffleTimeActions.jumpToTime,
startAutoReload: waffleTimeActions.startAutoReload,
stopAutoReload: waffleTimeActions.stopAutoReload,
})
);
export const WithWaffleTime = asChildFunctionRenderer(withWaffleTime, {
onCleanup: ({ stopAutoReload }) => stopAutoReload(),
});
/**
* Url State
*/
interface WaffleTimeUrlState {
time?: ReturnType<typeof waffleTimeSelectors.selectCurrentTime>;
autoReload?: ReturnType<typeof waffleTimeSelectors.selectIsAutoReloading>;
}
export const WithWaffleTimeUrlState = () => (
<WithWaffleTime>
{({ jumpToTime, startAutoReload, stopAutoReload, urlState }) => (
<UrlStateContainer
urlState={urlState}
urlStateKey="waffleTime"
mapToUrlState={mapToUrlState}
onChange={newUrlState => {
if (newUrlState && newUrlState.time) {
jumpToTime(newUrlState.time);
}
if (newUrlState && newUrlState.autoReload) {
startAutoReload();
} else if (
newUrlState &&
typeof newUrlState.autoReload !== 'undefined' &&
!newUrlState.autoReload
) {
stopAutoReload();
}
}}
onInitialize={initialUrlState => {
if (initialUrlState) {
jumpToTime(initialUrlState.time ? initialUrlState.time : Date.now());
}
if (initialUrlState && initialUrlState.autoReload) {
startAutoReload();
}
}}
/>
)}
</WithWaffleTime>
);
const selectTimeUrlState = createSelector(
waffleTimeSelectors.selectCurrentTime,
waffleTimeSelectors.selectIsAutoReloading,
(time, autoReload) => ({
time,
autoReload,
})
);
const mapToUrlState = (value: any): WaffleTimeUrlState | undefined =>
value
? {
time: mapToTimeUrlState(value.time),
autoReload: mapToAutoReloadUrlState(value.autoReload),
}
: undefined;
const mapToTimeUrlState = (value: any) => (value && typeof value === 'number' ? value : undefined);
const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined);

View file

@ -1,145 +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 { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { IIndexPattern } from 'src/plugins/data/public';
import {
State,
waffleOptionsActions,
waffleOptionsSelectors,
waffleTimeSelectors,
waffleTimeActions,
waffleFilterActions,
waffleFilterSelectors,
initialState,
} from '../../store';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { convertKueryToElasticSearchQuery } from '../../utils/kuery';
const selectViewState = createSelector(
waffleOptionsSelectors.selectMetric,
waffleOptionsSelectors.selectView,
waffleOptionsSelectors.selectGroupBy,
waffleOptionsSelectors.selectNodeType,
waffleOptionsSelectors.selectCustomOptions,
waffleOptionsSelectors.selectBoundsOverride,
waffleOptionsSelectors.selectAutoBounds,
waffleTimeSelectors.selectCurrentTime,
waffleTimeSelectors.selectIsAutoReloading,
waffleFilterSelectors.selectWaffleFilterQuery,
waffleOptionsSelectors.selectCustomMetrics,
(
metric,
view,
groupBy,
nodeType,
customOptions,
boundsOverride,
autoBounds,
time,
autoReload,
filterQuery,
customMetrics
) => ({
time,
autoReload,
metric,
groupBy,
nodeType,
view,
customOptions,
boundsOverride,
autoBounds,
filterQuery,
customMetrics,
})
);
interface Props {
indexPattern: IIndexPattern;
}
export const withWaffleViewState = connect(
(state: State) => ({
viewState: selectViewState(state),
defaultViewState: selectViewState(initialState),
}),
(dispatch, ownProps: Props) => {
return {
onViewChange: (viewState: WaffleViewState) => {
if (viewState.time) {
dispatch(waffleTimeActions.jumpToTime(viewState.time));
}
if (viewState.autoReload) {
dispatch(waffleTimeActions.startAutoReload());
} else if (typeof viewState.autoReload !== 'undefined' && !viewState.autoReload) {
dispatch(waffleTimeActions.stopAutoReload());
}
if (viewState.metric) {
dispatch(waffleOptionsActions.changeMetric(viewState.metric));
}
if (viewState.groupBy) {
dispatch(waffleOptionsActions.changeGroupBy(viewState.groupBy));
}
if (viewState.nodeType) {
dispatch(waffleOptionsActions.changeNodeType(viewState.nodeType));
}
if (viewState.view) {
dispatch(waffleOptionsActions.changeView(viewState.view));
}
if (viewState.customOptions) {
dispatch(waffleOptionsActions.changeCustomOptions(viewState.customOptions));
}
if (viewState.customMetrics) {
dispatch(waffleOptionsActions.changeCustomMetrics(viewState.customMetrics));
}
if (viewState.boundsOverride) {
dispatch(waffleOptionsActions.changeBoundsOverride(viewState.boundsOverride));
}
if (viewState.autoBounds) {
dispatch(waffleOptionsActions.changeAutoBounds(viewState.autoBounds));
}
if (viewState.filterQuery) {
dispatch(
waffleFilterActions.applyWaffleFilterQuery({
query: viewState.filterQuery,
serializedQuery: convertKueryToElasticSearchQuery(
viewState.filterQuery.expression,
ownProps.indexPattern
),
})
);
} else {
dispatch(
waffleFilterActions.applyWaffleFilterQuery({
query: null,
serializedQuery: null,
})
);
}
},
};
}
);
export const WithWaffleViewState = asChildFunctionRenderer(withWaffleViewState);
/**
* View State
*/
export interface WaffleViewState {
metric?: ReturnType<typeof waffleOptionsSelectors.selectMetric>;
groupBy?: ReturnType<typeof waffleOptionsSelectors.selectGroupBy>;
nodeType?: ReturnType<typeof waffleOptionsSelectors.selectNodeType>;
view?: ReturnType<typeof waffleOptionsSelectors.selectView>;
customOptions?: ReturnType<typeof waffleOptionsSelectors.selectCustomOptions>;
customMetrics?: ReturnType<typeof waffleOptionsSelectors.selectCustomMetrics>;
boundsOverride?: ReturnType<typeof waffleOptionsSelectors.selectBoundsOverride>;
autoBounds?: ReturnType<typeof waffleOptionsSelectors.selectAutoBounds>;
time?: ReturnType<typeof waffleTimeSelectors.selectCurrentTime>;
autoReload?: ReturnType<typeof waffleTimeSelectors.selectIsAutoReloading>;
filterQuery?: ReturnType<typeof waffleFilterSelectors.selectWaffleFilterQuery>;
}

View file

@ -8,7 +8,7 @@ import moment from 'moment';
import React from 'react';
import { euiPaletteColorBlind } from '@elastic/eui';
import { InfraFormatterType, InfraOptions, InfraWaffleMapLegendMode } from '../lib/lib';
import { InfraFormatterType, InfraOptions } from '../lib/lib';
import { RendererFunction } from '../utils/typed_react';
const euiVisColorPalette = euiPaletteColorBlind();
@ -29,7 +29,7 @@ const initialState = {
metric: { type: 'cpu' },
groupBy: [],
legend: {
type: InfraWaffleMapLegendMode.gradient,
type: 'gradient',
rules: [
{
value: 0,

View file

@ -136,18 +136,13 @@ export interface InfraWaffleMapGradientRule {
color: string;
}
export enum InfraWaffleMapLegendMode {
step = 'step',
gradient = 'gradient',
}
export interface InfraWaffleMapStepLegend {
type: InfraWaffleMapLegendMode.step;
type: 'step';
rules: InfraWaffleMapStepRule[];
}
export interface InfraWaffleMapGradientLegend {
type: InfraWaffleMapLegendMode.gradient;
type: 'gradient';
rules: InfraWaffleMapGradientRule[];
}

View file

@ -25,6 +25,9 @@ import { MetricsSettingsPage } from './settings';
import { AppNavigation } from '../../components/navigation/app_navigation';
import { SourceLoadingPage } from '../../components/source_loading_page';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { WaffleOptionsProvider } from '../inventory_view/hooks/use_waffle_options';
import { WaffleTimeProvider } from '../inventory_view/hooks/use_waffle_time';
import { WaffleFiltersProvider } from '../inventory_view/hooks/use_waffle_filters';
import { AlertDropdown } from '../../components/alerting/metrics/alert_dropdown';
export const InfrastructurePage = ({ match }: RouteComponentProps) => {
@ -32,96 +35,101 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
return (
<Source.Provider sourceId="default">
<ColumnarPage>
<DocumentTitle
title={i18n.translate('xpack.infra.homePage.documentTitle', {
defaultMessage: 'Metrics',
})}
/>
<WaffleOptionsProvider>
<WaffleTimeProvider>
<WaffleFiltersProvider>
<ColumnarPage>
<DocumentTitle
title={i18n.translate('xpack.infra.homePage.documentTitle', {
defaultMessage: 'Metrics',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/metrics"
appName={i18n.translate('xpack.infra.header.infrastructureHelpAppName', {
defaultMessage: 'Metrics',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/metrics"
appName={i18n.translate('xpack.infra.header.infrastructureHelpAppName', {
defaultMessage: 'Metrics',
})}
/>
<Header
breadcrumbs={[
{
text: i18n.translate('xpack.infra.header.infrastructureTitle', {
defaultMessage: 'Metrics',
}),
},
]}
readOnlyBadge={!uiCapabilities?.infrastructure?.save}
/>
<AppNavigation
aria-label={i18n.translate('xpack.infra.header.infrastructureNavigationTitle', {
defaultMessage: 'Metrics',
})}
>
<EuiFlexGroup gutterSize={'none'} alignItems={'center'}>
<EuiFlexItem>
<RoutedTabs
tabs={[
<Header
breadcrumbs={[
{
app: 'metrics',
title: i18n.translate('xpack.infra.homePage.inventoryTabTitle', {
defaultMessage: 'Inventory',
text: i18n.translate('xpack.infra.header.infrastructureTitle', {
defaultMessage: 'Metrics',
}),
pathname: '/inventory',
},
{
app: 'metrics',
title: i18n.translate('xpack.infra.homePage.metricsExplorerTabTitle', {
defaultMessage: 'Metrics Explorer',
}),
pathname: '/explorer',
},
{
app: 'metrics',
title: i18n.translate('xpack.infra.homePage.settingsTabTitle', {
defaultMessage: 'Settings',
}),
pathname: '/settings',
},
]}
readOnlyBadge={!uiCapabilities?.infrastructure?.save}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<Route path={'/explorer'} component={AlertDropdown} />
</EuiFlexItem>
</EuiFlexGroup>
</AppNavigation>
<AppNavigation
aria-label={i18n.translate('xpack.infra.header.infrastructureNavigationTitle', {
defaultMessage: 'Metrics',
})}
>
<EuiFlexGroup gutterSize={'none'} alignItems={'center'}>
<EuiFlexItem>
<RoutedTabs
tabs={[
{
app: 'metrics',
title: i18n.translate('xpack.infra.homePage.inventoryTabTitle', {
defaultMessage: 'Inventory',
}),
pathname: '/inventory',
},
{
app: 'metrics',
title: i18n.translate('xpack.infra.homePage.metricsExplorerTabTitle', {
defaultMessage: 'Metrics Explorer',
}),
pathname: '/explorer',
},
{
app: 'metrics',
title: i18n.translate('xpack.infra.homePage.settingsTabTitle', {
defaultMessage: 'Settings',
}),
pathname: '/settings',
},
]}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<Route path={'/explorer'} component={AlertDropdown} />
</EuiFlexItem>
</EuiFlexGroup>
</AppNavigation>
<Switch>
<Route path={'/inventory'} component={SnapshotPage} />
<Route
path={'/explorer'}
render={props => (
<WithSource>
{({ configuration, createDerivedIndexPattern }) => (
<MetricsExplorerOptionsContainer.Provider>
<WithMetricsExplorerOptionsUrlState />
{configuration ? (
<MetricsExplorerPage
derivedIndexPattern={createDerivedIndexPattern('metrics')}
source={configuration}
{...props}
/>
) : (
<SourceLoadingPage />
)}
</MetricsExplorerOptionsContainer.Provider>
)}
</WithSource>
)}
/>
<Route path={'/settings'} component={MetricsSettingsPage} />
</Switch>
</ColumnarPage>
<Switch>
<Route path={'/inventory'} component={SnapshotPage} />
<Route
path={'/explorer'}
render={props => (
<WithSource>
{({ configuration, createDerivedIndexPattern }) => (
<MetricsExplorerOptionsContainer.Provider>
<WithMetricsExplorerOptionsUrlState />
{configuration ? (
<MetricsExplorerPage
derivedIndexPattern={createDerivedIndexPattern('metrics')}
source={configuration}
{...props}
/>
) : (
<SourceLoadingPage />
)}
</MetricsExplorerOptionsContainer.Provider>
)}
</WithSource>
)}
/>
<Route path={'/settings'} component={MetricsSettingsPage} />
</Switch>
</ColumnarPage>
</WaffleFiltersProvider>
</WaffleTimeProvider>
</WaffleOptionsProvider>
</Source.Provider>
);
};

View file

@ -8,7 +8,6 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useContext } from 'react';
import { SnapshotPageContent } from './page_content';
import { SnapshotToolbar } from './toolbar';
import { DocumentTitle } from '../../../components/document_title';
@ -19,17 +18,14 @@ import { SourceErrorPage } from '../../../components/source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { ViewSourceConfigurationButton } from '../../../components/source_configuration';
import { Source } from '../../../containers/source';
import { WithWaffleFilterUrlState } from '../../../containers/waffle/with_waffle_filters';
import { WithWaffleOptionsUrlState } from '../../../containers/waffle/with_waffle_options';
import { WithWaffleTimeUrlState } from '../../../containers/waffle/with_waffle_time';
import { useTrackPageview } from '../../../../../observability/public';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { Layout } from '../../../components/inventory/layout';
import { useLinkProps } from '../../../hooks/use_link_props';
export const SnapshotPage = () => {
const uiCapabilities = useKibana().services.application?.capabilities;
const {
createDerivedIndexPattern,
hasFailedLoadingSource,
isLoading,
loadSourceFailureMessage,
@ -60,11 +56,8 @@ export const SnapshotPage = () => {
<SourceLoadingPage />
) : metricIndicesExist ? (
<>
<WithWaffleTimeUrlState />
<WithWaffleFilterUrlState indexPattern={createDerivedIndexPattern('metrics')} />
<WithWaffleOptionsUrlState />
<SnapshotToolbar />
<SnapshotPageContent />
<Layout />
</>
) : hasFailedLoadingSource ? (
<SourceErrorPage errorMessage={loadSourceFailureMessage || ''} retry={loadSource} />

View file

@ -1,68 +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 { WithWaffleFilter } from '../../../containers/waffle/with_waffle_filters';
import { WithWaffleOptions } from '../../../containers/waffle/with_waffle_options';
import { WithWaffleTime } from '../../../containers/waffle/with_waffle_time';
import { WithOptions } from '../../../containers/with_options';
import { WithSource } from '../../../containers/with_source';
import { Layout } from '../../../components/inventory/layout';
export const SnapshotPageContent: React.FC = () => (
<WithSource>
{({ configuration, createDerivedIndexPattern, sourceId }) => (
<WithOptions>
{({ wafflemap }) => (
<WithWaffleFilter indexPattern={createDerivedIndexPattern('metrics')}>
{({ filterQueryAsJson, applyFilterQuery }) => (
<WithWaffleTime>
{({ currentTime }) => (
<WithWaffleOptions>
{({
metric,
groupBy,
nodeType,
view,
changeView,
autoBounds,
boundsOverride,
accountId,
region,
}) => (
<Layout
currentTime={currentTime}
filterQuery={filterQueryAsJson}
metric={metric}
groupBy={groupBy}
nodeType={nodeType}
sourceId={sourceId}
options={{
...wafflemap,
metric,
fields: configuration && configuration.fields,
groupBy,
}}
onDrilldown={applyFilterQuery}
view={view}
onViewChange={changeView}
autoBounds={autoBounds}
boundsOverride={boundsOverride}
accountId={accountId}
region={region}
/>
)}
</WithWaffleOptions>
)}
</WithWaffleTime>
)}
</WithWaffleFilter>
)}
</WithOptions>
)}
</WithSource>
);

View file

@ -5,92 +5,24 @@
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { AutocompleteField } from '../../../components/autocomplete_field';
import { Toolbar } from '../../../components/eui/toolbar';
import { WaffleTimeControls } from '../../../components/waffle/waffle_time_controls';
import { WithWaffleFilter } from '../../../containers/waffle/with_waffle_filters';
import { WithWaffleTime } from '../../../containers/waffle/with_waffle_time';
import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion';
import { WithSource } from '../../../containers/with_source';
import { WithWaffleOptions } from '../../../containers/waffle/with_waffle_options';
import { WaffleInventorySwitcher } from '../../../components/waffle/waffle_inventory_switcher';
import { SearchBar } from '../../inventory_view/compontents/search_bar';
export const SnapshotToolbar = () => (
<Toolbar>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="m">
<EuiFlexItem grow={false}>
<WithWaffleOptions>
{({
changeMetric,
changeNodeType,
changeGroupBy,
changeAccount,
changeRegion,
changeCustomMetrics,
nodeType,
}) => (
<WaffleInventorySwitcher
nodeType={nodeType}
changeNodeType={changeNodeType}
changeMetric={changeMetric}
changeGroupBy={changeGroupBy}
changeAccount={changeAccount}
changeRegion={changeRegion}
changeCustomMetrics={changeCustomMetrics}
/>
)}
</WithWaffleOptions>
<WaffleInventorySwitcher />
</EuiFlexItem>
<EuiFlexItem>
<WithSource>
{({ createDerivedIndexPattern }) => (
<WithKueryAutocompletion indexPattern={createDerivedIndexPattern('metrics')}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<WithWaffleFilter indexPattern={createDerivedIndexPattern('metrics')}>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={i18n.translate(
'xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder',
{
defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)',
}
)}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
autoFocus={true}
/>
)}
</WithWaffleFilter>
)}
</WithKueryAutocompletion>
)}
</WithSource>
<SearchBar />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<WithWaffleTime resetOnUnmount>
{({ currentTime, isAutoReloading, jumpToTime, startAutoReload, stopAutoReload }) => (
<WaffleTimeControls
currentTime={currentTime}
isLiveStreaming={isAutoReloading}
onChangeTime={jumpToTime}
startLiveStreaming={startAutoReload}
stopLiveStreaming={stopAutoReload}
/>
)}
</WithWaffleTime>
<WaffleTimeControls />
</EuiFlexItem>
</EuiFlexGroup>
</Toolbar>

View file

@ -0,0 +1,40 @@
/*
* 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, { useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { Source } from '../../../containers/source';
import { AutocompleteField } from '../../../components/autocomplete_field';
import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion';
import { useWaffleFiltersContext } from '../hooks/use_waffle_filters';
export const SearchBar = () => {
const { createDerivedIndexPattern } = useContext(Source.Context);
const {
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
} = useWaffleFiltersContext();
return (
<WithKueryAutocompletion indexPattern={createDerivedIndexPattern('metrics')}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={i18n.translate('xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder', {
defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)',
})}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft : ''}
autoFocus={true}
/>
)}
</WithKueryAutocompletion>
);
};

View file

@ -0,0 +1,93 @@
/*
* 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 { useState, useMemo, useCallback, useEffect } from 'react';
import * as rt from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import createContainter from 'constate';
import { useUrlState } from '../../../utils/use_url_state';
import { useSourceContext } from '../../../containers/source';
import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';
import { esKuery } from '../../../../../../../src/plugins/data/public';
const validateKuery = (expression: string) => {
try {
esKuery.fromKueryExpression(expression);
} catch (err) {
return false;
}
return true;
};
export const DEFAULT_WAFFLE_FILTERS_STATE: WaffleFiltersState = { kind: 'kuery', expression: '' };
export const useWaffleFilters = () => {
const { createDerivedIndexPattern } = useSourceContext();
const indexPattern = createDerivedIndexPattern('metrics');
const [urlState, setUrlState] = useUrlState<WaffleFiltersState>({
defaultState: DEFAULT_WAFFLE_FILTERS_STATE,
decodeUrlState,
encodeUrlState,
urlStateKey: 'waffleFilter',
});
const [state, setState] = useState<WaffleFiltersState>(urlState);
useEffect(() => setUrlState(state), [setUrlState, state]);
const [filterQueryDraft, setFilterQueryDraft] = useState<string>(urlState.expression);
const filterQueryAsJson = useMemo(
() => convertKueryToElasticSearchQuery(urlState.expression, indexPattern),
[indexPattern, urlState.expression]
);
const applyFilterQueryFromKueryExpression = useCallback(
(expression: string) => {
setState(previous => ({
...previous,
kind: 'kuery',
expression,
}));
},
[setState]
);
const applyFilterQuery = useCallback((filterQuery: WaffleFiltersState) => {
setState(filterQuery);
setFilterQueryDraft(filterQuery.expression);
}, []);
const isFilterQueryDraftValid = useMemo(() => validateKuery(filterQueryDraft), [
filterQueryDraft,
]);
return {
filterQuery: urlState,
filterQueryDraft,
filterQueryAsJson,
applyFilterQuery,
setFilterQueryDraftFromKueryExpression: setFilterQueryDraft,
applyFilterQueryFromKueryExpression,
isFilterQueryDraftValid,
setWaffleFiltersState: applyFilterQuery,
};
};
export const WaffleFiltersStateRT = rt.type({
kind: rt.literal('kuery'),
expression: rt.string,
});
export type WaffleFiltersState = rt.TypeOf<typeof WaffleFiltersStateRT>;
const encodeUrlState = WaffleFiltersStateRT.encode;
const decodeUrlState = (value: unknown) =>
pipe(WaffleFiltersStateRT.decode(value), fold(constant(undefined), identity));
export const WaffleFilters = createContainter(useWaffleFilters);
export const [WaffleFiltersProvider, useWaffleFiltersContext] = WaffleFilters;

View file

@ -0,0 +1,147 @@
/*
* 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 { useCallback, useState, useEffect } from 'react';
import * as rt from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import createContainer from 'constate';
import {
SnapshotMetricInput,
SnapshotGroupBy,
SnapshotCustomMetricInput,
SnapshotMetricInputRT,
SnapshotGroupByRT,
SnapshotCustomMetricInputRT,
} from '../../../../common/http_api/snapshot_api';
import { useUrlState } from '../../../utils/use_url_state';
import { InventoryItemType, ItemTypeRT } from '../../../../common/inventory_models/types';
export const DEFAULT_WAFFLE_OPTIONS_STATE: WaffleOptionsState = {
metric: { type: 'cpu' },
groupBy: [],
nodeType: 'host',
view: 'map',
customOptions: [],
boundsOverride: { max: 1, min: 0 },
autoBounds: true,
accountId: '',
region: '',
customMetrics: [],
};
export const useWaffleOptions = () => {
const [urlState, setUrlState] = useUrlState<WaffleOptionsState>({
defaultState: DEFAULT_WAFFLE_OPTIONS_STATE,
decodeUrlState,
encodeUrlState,
urlStateKey: 'waffleOptions',
});
const [state, setState] = useState<WaffleOptionsState>(urlState);
useEffect(() => setUrlState(state), [setUrlState, state]);
const changeMetric = useCallback(
(metric: SnapshotMetricInput) => setState(previous => ({ ...previous, metric })),
[setState]
);
const changeGroupBy = useCallback(
(groupBy: SnapshotGroupBy) => setState(previous => ({ ...previous, groupBy })),
[setState]
);
const changeNodeType = useCallback(
(nodeType: InventoryItemType) => setState(previous => ({ ...previous, nodeType })),
[setState]
);
const changeView = useCallback((view: string) => setState(previous => ({ ...previous, view })), [
setState,
]);
const changeCustomOptions = useCallback(
(customOptions: Array<{ text: string; field: string }>) =>
setState(previous => ({ ...previous, customOptions })),
[setState]
);
const changeAutoBounds = useCallback(
(autoBounds: boolean) => setState(previous => ({ ...previous, autoBounds })),
[setState]
);
const changeBoundsOverride = useCallback(
(boundsOverride: { min: number; max: number }) =>
setState(previous => ({ ...previous, boundsOverride })),
[setState]
);
const changeAccount = useCallback(
(accountId: string) => setState(previous => ({ ...previous, accountId })),
[setState]
);
const changeRegion = useCallback(
(region: string) => setState(previous => ({ ...previous, region })),
[setState]
);
const changeCustomMetrics = useCallback(
(customMetrics: SnapshotCustomMetricInput[]) => {
setState(previous => ({ ...previous, customMetrics }));
},
[setState]
);
return {
...state,
changeMetric,
changeGroupBy,
changeNodeType,
changeView,
changeCustomOptions,
changeAutoBounds,
changeBoundsOverride,
changeAccount,
changeRegion,
changeCustomMetrics,
setWaffleOptionsState: setState,
};
};
export const WaffleOptionsStateRT = rt.type({
metric: SnapshotMetricInputRT,
groupBy: SnapshotGroupByRT,
nodeType: ItemTypeRT,
view: rt.string,
customOptions: rt.array(
rt.type({
text: rt.string,
field: rt.string,
})
),
boundsOverride: rt.type({
min: rt.number,
max: rt.number,
}),
autoBounds: rt.boolean,
accountId: rt.string,
region: rt.string,
customMetrics: rt.array(SnapshotCustomMetricInputRT),
});
export type WaffleOptionsState = rt.TypeOf<typeof WaffleOptionsStateRT>;
const encodeUrlState = (state: WaffleOptionsState) => {
return WaffleOptionsStateRT.encode(state);
};
const decodeUrlState = (value: unknown) =>
pipe(WaffleOptionsStateRT.decode(value), fold(constant(undefined), identity));
export const WaffleOptions = createContainer(useWaffleOptions);
export const [WaffleOptionsProvider, useWaffleOptionsContext] = WaffleOptions;

View file

@ -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 { useCallback, useState, useEffect } from 'react';
import * as rt from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import createContainer from 'constate';
import { useUrlState } from '../../../utils/use_url_state';
export const DEFAULT_WAFFLE_TIME_STATE: WaffleTimeState = {
currentTime: Date.now(),
isAutoReloading: false,
};
export const useWaffleTime = () => {
const [urlState, setUrlState] = useUrlState<WaffleTimeState>({
defaultState: DEFAULT_WAFFLE_TIME_STATE,
decodeUrlState,
encodeUrlState,
urlStateKey: 'waffleTime',
});
const [state, setState] = useState<WaffleTimeState>(urlState);
useEffect(() => setUrlState(state), [setUrlState, state]);
const { currentTime, isAutoReloading } = urlState;
const startAutoReload = useCallback(() => {
setState(previous => ({ ...previous, isAutoReloading: true }));
}, [setState]);
const stopAutoReload = useCallback(() => {
setState(previous => ({ ...previous, isAutoReloading: false }));
}, [setState]);
const jumpToTime = useCallback(
(time: number) => {
setState(previous => ({ ...previous, currentTime: time }));
},
[setState]
);
const currentTimeRange = {
from: currentTime - 1000 * 60 * 5,
interval: '1m',
to: currentTime,
};
return {
currentTime,
currentTimeRange,
isAutoReloading,
startAutoReload,
stopAutoReload,
jumpToTime,
setWaffleTimeState: setState,
};
};
export const WaffleTimeStateRT = rt.type({
currentTime: rt.number,
isAutoReloading: rt.boolean,
});
export type WaffleTimeState = rt.TypeOf<typeof WaffleTimeStateRT>;
const encodeUrlState = WaffleTimeStateRT.encode;
const decodeUrlState = (value: unknown) =>
pipe(WaffleTimeStateRT.decode(value), fold(constant(undefined), identity));
export const WaffleTime = createContainer(useWaffleTime);
export const [WaffleTimeProvider, useWaffleTimeContext] = WaffleTime;

View file

@ -0,0 +1,95 @@
/*
* 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 { useCallback } from 'react';
import {
useWaffleOptionsContext,
DEFAULT_WAFFLE_OPTIONS_STATE,
WaffleOptionsState,
} from './use_waffle_options';
import { useWaffleTimeContext, DEFAULT_WAFFLE_TIME_STATE } from './use_waffle_time';
import {
useWaffleFiltersContext,
DEFAULT_WAFFLE_FILTERS_STATE,
WaffleFiltersState,
} from './use_waffle_filters';
export const useWaffleViewState = () => {
const {
metric,
groupBy,
nodeType,
view,
customOptions,
customMetrics,
boundsOverride,
autoBounds,
accountId,
region,
setWaffleOptionsState,
} = useWaffleOptionsContext();
const { currentTime, isAutoReloading, setWaffleTimeState } = useWaffleTimeContext();
const { filterQuery, setWaffleFiltersState } = useWaffleFiltersContext();
const viewState: WaffleViewState = {
metric,
groupBy,
nodeType,
view,
customOptions,
customMetrics,
boundsOverride,
autoBounds,
accountId,
region,
time: currentTime,
autoReload: isAutoReloading,
filterQuery,
};
const defaultViewState: WaffleViewState = {
...DEFAULT_WAFFLE_OPTIONS_STATE,
filterQuery: DEFAULT_WAFFLE_FILTERS_STATE,
time: DEFAULT_WAFFLE_TIME_STATE.currentTime,
autoReload: DEFAULT_WAFFLE_TIME_STATE.isAutoReloading,
};
const onViewChange = useCallback(
(newState: WaffleViewState) => {
setWaffleOptionsState({
metric: newState.metric,
groupBy: newState.groupBy,
nodeType: newState.nodeType,
view: newState.view,
customOptions: newState.customOptions,
customMetrics: newState.customMetrics,
boundsOverride: newState.boundsOverride,
autoBounds: newState.autoBounds,
accountId: newState.accountId,
region: newState.region,
});
if (newState.time) {
setWaffleTimeState({
currentTime: newState.time,
isAutoReloading: newState.autoReload,
});
}
setWaffleFiltersState(newState.filterQuery);
},
[setWaffleOptionsState, setWaffleTimeState, setWaffleFiltersState]
);
return {
viewState,
defaultViewState,
onViewChange,
};
};
export type WaffleViewState = WaffleOptionsState & {
time: number;
autoReload: boolean;
filterQuery: WaffleFiltersState;
};

View file

@ -8,7 +8,7 @@ import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { replaceMetricTimeInQueryString } from '../metrics/containers/with_metrics_time';
import { replaceMetricTimeInQueryString } from '../metrics/hooks/use_metrics_time';
import { useHostIpToName } from './use_host_ip_to_name';
import { getFromFromLocation, getToFromLocation } from './query_params';
import { LoadingPage } from '../../components/loading_page';

View file

@ -7,7 +7,7 @@
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { replaceMetricTimeInQueryString } from '../metrics/containers/with_metrics_time';
import { replaceMetricTimeInQueryString } from '../metrics/hooks/use_metrics_time';
import { getFromFromLocation, getToFromLocation } from './query_params';
import { InventoryItemType } from '../../../common/inventory_models/types';
import { LinkDescriptor } from '../../hooks/use_link_props';

View file

@ -22,7 +22,7 @@ import { MetricsTimeControls } from './time_controls';
import { SideNavContext, NavItem } from '../lib/side_nav_context';
import { PageBody } from './page_body';
import { euiStyled } from '../../../../../observability/public';
import { MetricsTimeInput } from '../containers/with_metrics_time';
import { MetricsTimeInput } from '../hooks/use_metrics_time';
import { InfraMetadata } from '../../../../common/http_api/metadata_api';
import { PageError } from './page_error';
import { MetadataContext } from '../../../pages/metrics/containers/metadata_context';
@ -94,7 +94,7 @@ export const NodeDetailsPage = (props: Props) => {
setRefreshInterval={props.setRefreshInterval}
onChangeTimeRange={props.setTimeRange}
setAutoReload={props.setAutoReload}
onRefresh={props.triggerRefresh}
onRefresh={refetch}
/>
</MetricsTitleTimeRangeContainer>
</EuiPageHeaderSection>

View file

@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
import { findLayout } from '../../../../common/inventory_models/layouts';
import { InventoryItemType } from '../../../../common/inventory_models/types';
import { MetricsTimeInput } from '../containers/with_metrics_time';
import { MetricsTimeInput } from '../hooks/use_metrics_time';
import { InfraLoadingPanel } from '../../../components/loading';
import { NoData } from '../../../components/empty_states';
import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api';
@ -19,9 +19,9 @@ interface Props {
refetch: () => void;
type: InventoryItemType;
metrics: NodeDetailsMetricData[];
onChangeRangeTime?: (time: MetricsTimeInput) => void;
isLiveStreaming?: boolean;
stopLiveStreaming?: () => void;
onChangeRangeTime: (time: MetricsTimeInput) => void;
isLiveStreaming: boolean;
stopLiveStreaming: () => void;
}
export const PageBody = ({

View file

@ -41,6 +41,9 @@ export const Section: FunctionComponent<SectionProps> = ({
if (metric === null) {
return accumulatedChildren;
}
if (!child.props.label) {
return accumulatedChildren;
}
return [
...accumulatedChildren,
{

View file

@ -19,7 +19,7 @@ jest.mock('../../../utils/use_kibana_ui_setting', () => ({
import React from 'react';
import { MetricsTimeControls } from './time_controls';
import { mount } from 'enzyme';
import { MetricsTimeInput } from '../containers/with_metrics_time';
import { MetricsTimeInput } from '../hooks/use_metrics_time';
describe('MetricsTimeControls', () => {
it('should set a valid from and to value for Today', () => {

View file

@ -7,7 +7,7 @@
import { EuiSuperDatePicker, OnRefreshChangeProps, OnTimeChangeProps } from '@elastic/eui';
import React, { useCallback } from 'react';
import { euiStyled } from '../../../../../observability/public';
import { MetricsTimeInput } from '../containers/with_metrics_time';
import { MetricsTimeInput } from '../hooks/use_metrics_time';
import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting';
import { mapKibanaQuickRangesToDatePickerRanges } from '../../../utils/map_timepicker_quickranges_to_datepicker_ranges';
@ -61,8 +61,8 @@ export const MetricsTimeControls = (props: MetricsTimeControlsProps) => {
return (
<MetricsTimeControlsContainer>
<EuiSuperDatePicker
start={currentTimeRange.from}
end={currentTimeRange.to}
start={currentTimeRange.from.toString()}
end={currentTimeRange.to.toString()}
isPaused={!isLiveStreaming}
refreshInterval={refreshInterval ? refreshInterval : 0}
onTimeChange={handleTimeChange}

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 gql from 'graphql-tag';
export const metricsQuery = gql`
query MetricsQuery(
$sourceId: ID!
$timerange: InfraTimerangeInput!
$metrics: [InfraMetric!]!
$nodeId: ID!
$cloudId: ID
$nodeType: InfraNodeType!
) {
source(id: $sourceId) {
id
metrics(
nodeIds: { nodeId: $nodeId, cloudId: $cloudId }
timerange: $timerange
metrics: $metrics
nodeType: $nodeType
) {
id
series {
id
label
data {
timestamp
value
}
}
}
}
}
`;

View file

@ -1,201 +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 createContainer from 'constate';
import React, { useContext, useState, useCallback } from 'react';
import { isNumber } from 'lodash';
import moment from 'moment';
import dateMath from '@elastic/datemath';
import * as rt from 'io-ts';
import { isRight } from 'fp-ts/lib/Either';
import { replaceStateKeyInQueryString, UrlStateContainer } from '../../../utils/url_state';
import { InfraTimerangeInput } from '../../../graphql/types';
export interface MetricsTimeInput {
from: string;
to: string;
interval: string;
}
interface MetricsTimeState {
timeRange: MetricsTimeInput;
parsedTimeRange: InfraTimerangeInput;
setTimeRange: (timeRange: MetricsTimeInput) => void;
refreshInterval: number;
setRefreshInterval: (refreshInterval: number) => void;
isAutoReloading: boolean;
setAutoReload: (isAutoReloading: boolean) => void;
lastRefresh: number;
triggerRefresh: () => void;
}
const parseRange = (range: MetricsTimeInput) => {
const parsedFrom = dateMath.parse(range.from);
const parsedTo = dateMath.parse(range.to, { roundUp: true });
return {
...range,
from:
(parsedFrom && parsedFrom.valueOf()) ||
moment()
.subtract(1, 'hour')
.valueOf(),
to: (parsedTo && parsedTo.valueOf()) || moment().valueOf(),
};
};
export const useMetricsTime = () => {
const defaultRange = {
from: 'now-1h',
to: 'now',
interval: '>=1m',
};
const [isAutoReloading, setAutoReload] = useState(false);
const [refreshInterval, setRefreshInterval] = useState(5000);
const [lastRefresh, setLastRefresh] = useState<number>(moment().valueOf());
const [timeRange, setTimeRange] = useState(defaultRange);
const [parsedTimeRange, setParsedTimeRange] = useState(parseRange(defaultRange));
const updateTimeRange = useCallback((range: MetricsTimeInput) => {
setTimeRange(range);
setParsedTimeRange(parseRange(range));
}, []);
return {
timeRange,
setTimeRange: updateTimeRange,
parsedTimeRange,
refreshInterval,
setRefreshInterval,
isAutoReloading,
setAutoReload,
lastRefresh,
triggerRefresh: useCallback(() => setLastRefresh(moment().valueOf()), [setLastRefresh]),
};
};
export const MetricsTimeContainer = createContainer(useMetricsTime);
interface WithMetricsTimeProps {
children: (args: MetricsTimeState) => React.ReactElement;
}
export const WithMetricsTime: React.FunctionComponent<WithMetricsTimeProps> = ({
children,
}: WithMetricsTimeProps) => {
const metricsTimeState = useContext(MetricsTimeContainer.Context);
return children({ ...metricsTimeState });
};
/**
* Url State
*/
interface MetricsTimeUrlState {
time?: MetricsTimeState['timeRange'];
autoReload?: boolean;
refreshInterval?: number;
}
export const WithMetricsTimeUrlState = () => (
<WithMetricsTime>
{({
timeRange,
setTimeRange,
refreshInterval,
setRefreshInterval,
isAutoReloading,
setAutoReload,
}) => (
<UrlStateContainer
urlState={{
time: timeRange,
autoReload: isAutoReloading,
refreshInterval,
}}
urlStateKey="metricTime"
mapToUrlState={mapToUrlState}
onChange={newUrlState => {
if (newUrlState && newUrlState.time) {
setTimeRange(newUrlState.time);
}
if (newUrlState && newUrlState.autoReload) {
setAutoReload(true);
} else if (
newUrlState &&
typeof newUrlState.autoReload !== 'undefined' &&
!newUrlState.autoReload
) {
setAutoReload(false);
}
if (newUrlState && newUrlState.refreshInterval) {
setRefreshInterval(newUrlState.refreshInterval);
}
}}
onInitialize={initialUrlState => {
if (initialUrlState && initialUrlState.time) {
if (
timeRange.from !== initialUrlState.time.from ||
timeRange.to !== initialUrlState.time.to ||
timeRange.interval !== initialUrlState.time.interval
) {
setTimeRange(initialUrlState.time);
}
}
if (initialUrlState && initialUrlState.autoReload) {
setAutoReload(true);
}
if (initialUrlState && initialUrlState.refreshInterval) {
setRefreshInterval(initialUrlState.refreshInterval);
}
}}
/>
)}
</WithMetricsTime>
);
const mapToUrlState = (value: any): MetricsTimeUrlState | undefined =>
value
? {
time: mapToTimeUrlState(value.time),
autoReload: mapToAutoReloadUrlState(value.autoReload),
refreshInterval: mapToRefreshInterval(value.refreshInterval),
}
: undefined;
const MetricsTimeRT = rt.type({
from: rt.union([rt.string, rt.number]),
to: rt.union([rt.string, rt.number]),
interval: rt.string,
});
const mapToTimeUrlState = (value: any) => {
const result = MetricsTimeRT.decode(value);
if (isRight(result)) {
const resultValue = result.right;
const to = isNumber(resultValue.to) ? moment(resultValue.to).toISOString() : resultValue.to;
const from = isNumber(resultValue.from)
? moment(resultValue.from).toISOString()
: resultValue.from;
return { ...resultValue, from, to };
}
return undefined;
};
const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined);
const mapToRefreshInterval = (value: any) => (typeof value === 'number' ? value : undefined);
export const replaceMetricTimeInQueryString = (from: number, to: number) =>
Number.isNaN(from) || Number.isNaN(to)
? (value: string) => value
: replaceStateKeyInQueryString<MetricsTimeUrlState>('metricTime', {
autoReload: false,
time: {
interval: '>=1m',
from: moment(from).toISOString(),
to: moment(to).toISOString(),
},
});

View file

@ -6,7 +6,7 @@
import { mountHook } from 'test_utils/enzyme_helpers';
import { useMetricsTime } from './with_metrics_time';
import { useMetricsTime } from './use_metrics_time';
describe('useMetricsTime hook', () => {
describe('timeRange state', () => {

View file

@ -0,0 +1,121 @@
/*
* 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 createContainer from 'constate';
import { useState, useCallback, useEffect } from 'react';
import moment from 'moment';
import dateMath from '@elastic/datemath';
import * as rt from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import { useUrlState } from '../../../utils/use_url_state';
import { replaceStateKeyInQueryString } from '../../../utils/url_state';
const parseRange = (range: MetricsTimeInput) => {
const parsedFrom = dateMath.parse(range.from.toString());
const parsedTo = dateMath.parse(range.to.toString(), { roundUp: true });
return {
...range,
from:
(parsedFrom && parsedFrom.valueOf()) ||
moment()
.subtract(1, 'hour')
.valueOf(),
to: (parsedTo && parsedTo.valueOf()) || moment().valueOf(),
};
};
const DEFAULT_TIMERANGE: MetricsTimeInput = {
from: 'now-1h',
to: 'now',
interval: '>=1m',
};
const DEFAULT_URL_STATE: MetricsTimeUrlState = {
time: DEFAULT_TIMERANGE,
autoReload: false,
refreshInterval: 5000,
};
export const useMetricsTime = () => {
const [urlState, setUrlState] = useUrlState<MetricsTimeUrlState>({
defaultState: DEFAULT_URL_STATE,
decodeUrlState,
encodeUrlState,
urlStateKey: 'metricTime',
});
const [isAutoReloading, setAutoReload] = useState(urlState.autoReload || false);
const [refreshInterval, setRefreshInterval] = useState(urlState.refreshInterval || 5000);
const [lastRefresh, setLastRefresh] = useState<number>(moment().valueOf());
const [timeRange, setTimeRange] = useState(urlState.time || DEFAULT_TIMERANGE);
useEffect(() => {
const newState = {
time: timeRange,
autoReload: isAutoReloading,
refreshInterval,
};
return setUrlState(newState);
}, [isAutoReloading, refreshInterval, setUrlState, timeRange]);
const [parsedTimeRange, setParsedTimeRange] = useState(
parseRange(urlState.time || DEFAULT_TIMERANGE)
);
const updateTimeRange = useCallback((range: MetricsTimeInput) => {
setTimeRange(range);
setParsedTimeRange(parseRange(range));
}, []);
return {
timeRange,
setTimeRange: updateTimeRange,
parsedTimeRange,
refreshInterval,
setRefreshInterval,
isAutoReloading,
setAutoReload,
lastRefresh,
triggerRefresh: useCallback(() => {
return setLastRefresh(moment().valueOf());
}, [setLastRefresh]),
};
};
export const MetricsTimeInputRT = rt.type({
from: rt.union([rt.string, rt.number]),
to: rt.union([rt.string, rt.number]),
interval: rt.string,
});
export type MetricsTimeInput = rt.TypeOf<typeof MetricsTimeInputRT>;
export const MetricsTimeUrlStateRT = rt.partial({
time: MetricsTimeInputRT,
autoReload: rt.boolean,
refreshInterval: rt.number,
});
export type MetricsTimeUrlState = rt.TypeOf<typeof MetricsTimeUrlStateRT>;
const encodeUrlState = MetricsTimeUrlStateRT.encode;
const decodeUrlState = (value: unknown) =>
pipe(MetricsTimeUrlStateRT.decode(value), fold(constant(undefined), identity));
export const replaceMetricTimeInQueryString = (from: number, to: number) =>
Number.isNaN(from) || Number.isNaN(to)
? (value: string) => value
: replaceStateKeyInQueryString<MetricsTimeUrlState>('metricTime', {
autoReload: false,
time: {
interval: '>=1m',
from: moment(from).toISOString(),
to: moment(to).toISOString(),
},
});
export const MetricsTimeContainer = createContainer(useMetricsTime);
export const [MetricsTimeProvider, useMetricsTimeContext] = MetricsTimeContainer;

View file

@ -9,7 +9,6 @@ import { euiStyled, EuiTheme, withTheme } from '../../../../observability/public
import { DocumentTitle } from '../../components/document_title';
import { Header } from '../../components/header';
import { ColumnarPage, PageContent } from '../../components/page';
import { WithMetricsTime, WithMetricsTimeUrlState } from './containers/with_metrics_time';
import { withMetricPageProviders } from './page_providers';
import { useMetadata } from '../../containers/metadata/use_metadata';
import { Source } from '../../containers/source';
@ -19,6 +18,7 @@ import { NavItem } from './lib/side_nav_context';
import { NodeDetailsPage } from './components/node_details_page';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { InventoryItemType } from '../../../common/inventory_models/types';
import { useMetricsTimeContext } from './hooks/use_metrics_time';
import { useLinkProps } from '../../hooks/use_link_props';
const DetailPageContent = euiStyled(PageContent)`
@ -37,19 +37,29 @@ interface Props {
}
export const MetricDetail = withMetricPageProviders(
withTheme(({ match, theme }: Props) => {
withTheme(({ match }: Props) => {
const uiCapabilities = useKibana().services.application?.capabilities;
const nodeId = match.params.node;
const nodeType = match.params.type as InventoryItemType;
const inventoryModel = findInventoryModel(nodeType);
const { sourceId } = useContext(Source.Context);
const {
timeRange,
parsedTimeRange,
setTimeRange,
refreshInterval,
setRefreshInterval,
isAutoReloading,
setAutoReload,
triggerRefresh,
} = useMetricsTimeContext();
const {
name,
filteredRequiredMetrics,
loading: metadataLoading,
cloudId,
metadata,
} = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId);
} = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId, parsedTimeRange);
const [sideNav, setSideNav] = useState<NavItem[]>([]);
@ -90,58 +100,41 @@ export const MetricDetail = withMetricPageProviders(
}
return (
<WithMetricsTime>
{({
timeRange,
parsedTimeRange,
setTimeRange,
refreshInterval,
setRefreshInterval,
isAutoReloading,
setAutoReload,
triggerRefresh,
}) => (
<ColumnarPage>
<Header
breadcrumbs={breadcrumbs}
readOnlyBadge={!uiCapabilities?.infrastructure?.save}
<ColumnarPage>
<Header breadcrumbs={breadcrumbs} readOnlyBadge={!uiCapabilities?.infrastructure?.save} />
<DocumentTitle
title={i18n.translate('xpack.infra.metricDetailPage.documentTitle', {
defaultMessage: 'Infrastructure | Metrics | {name}',
values: {
name,
},
})}
/>
<DetailPageContent data-test-subj="infraMetricsPage">
{metadata ? (
<NodeDetailsPage
name={name}
requiredMetrics={filteredRequiredMetrics}
sourceId={sourceId}
timeRange={timeRange}
parsedTimeRange={parsedTimeRange}
nodeType={nodeType}
nodeId={nodeId}
cloudId={cloudId}
metadataLoading={metadataLoading}
isAutoReloading={isAutoReloading}
refreshInterval={refreshInterval}
sideNav={sideNav}
metadata={metadata}
addNavItem={addNavItem}
setRefreshInterval={setRefreshInterval}
setAutoReload={setAutoReload}
triggerRefresh={triggerRefresh}
setTimeRange={setTimeRange}
/>
<WithMetricsTimeUrlState />
<DocumentTitle
title={i18n.translate('xpack.infra.metricDetailPage.documentTitle', {
defaultMessage: 'Infrastructure | Metrics | {name}',
values: {
name,
},
})}
/>
<DetailPageContent data-test-subj="infraMetricsPage">
{metadata ? (
<NodeDetailsPage
name={name}
requiredMetrics={filteredRequiredMetrics}
sourceId={sourceId}
timeRange={timeRange}
parsedTimeRange={parsedTimeRange}
nodeType={nodeType}
nodeId={nodeId}
cloudId={cloudId}
metadataLoading={metadataLoading}
isAutoReloading={isAutoReloading}
refreshInterval={refreshInterval}
sideNav={sideNav}
metadata={metadata}
addNavItem={addNavItem}
setRefreshInterval={setRefreshInterval}
setAutoReload={setAutoReload}
triggerRefresh={triggerRefresh}
setTimeRange={setTimeRange}
/>
) : null}
</DetailPageContent>
</ColumnarPage>
)}
</WithMetricsTime>
) : null}
</DetailPageContent>
</ColumnarPage>
);
})
);

View file

@ -6,15 +6,15 @@
import React from 'react';
import { MetricsTimeContainer } from './containers/with_metrics_time';
import { Source } from '../../containers/source';
import { MetricsTimeProvider } from './hooks/use_metrics_time';
export const withMetricPageProviders = <T extends object>(Component: React.ComponentType<T>) => (
props: T
) => (
<Source.Provider sourceId="default">
<MetricsTimeContainer.Provider>
<MetricsTimeProvider>
<Component {...props} />
</MetricsTimeContainer.Provider>
</MetricsTimeProvider>
</Source.Provider>
);

View file

@ -7,7 +7,7 @@
import rt from 'io-ts';
import { EuiTheme } from '../../../../observability/public';
import { InventoryFormatterTypeRT } from '../../../common/inventory_models/types';
import { MetricsTimeInput } from './containers/with_metrics_time';
import { MetricsTimeInput } from './hooks/use_metrics_time';
import { NodeDetailsMetricData } from '../../../common/http_api/node_details_api';
export interface LayoutProps {

View file

@ -1,7 +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.
*/
export { waffleFilterActions, waffleTimeActions, waffleOptionsActions } from './local';

View file

@ -1,11 +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 { combineEpics } from 'redux-observable';
import { createLocalEpic } from './local';
export const createRootEpic = <State>() => combineEpics(createLocalEpic<State>());

View file

@ -1,11 +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.
*/
export * from './actions';
export * from './epics';
export * from './reducer';
export * from './selectors';
export { createStore } from './store';

View file

@ -1,9 +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.
*/
export { waffleFilterActions } from './waffle_filter';
export { waffleTimeActions } from './waffle_time';
export { waffleOptionsActions } from './waffle_options';

View file

@ -1,11 +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 { combineEpics } from 'redux-observable';
import { createWaffleTimeEpic } from './waffle_time';
export const createLocalEpic = <State>() => combineEpics(createWaffleTimeEpic<State>());

View file

@ -1,10 +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.
*/
export * from './actions';
export * from './epic';
export * from './reducer';
export * from './selectors';

View file

@ -1,33 +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 { combineReducers } from 'redux';
import { initialWaffleFilterState, waffleFilterReducer, WaffleFilterState } from './waffle_filter';
import {
initialWaffleOptionsState,
waffleOptionsReducer,
WaffleOptionsState,
} from './waffle_options';
import { initialWaffleTimeState, waffleTimeReducer, WaffleTimeState } from './waffle_time';
export interface LocalState {
waffleFilter: WaffleFilterState;
waffleTime: WaffleTimeState;
waffleMetrics: WaffleOptionsState;
}
export const initialLocalState: LocalState = {
waffleFilter: initialWaffleFilterState,
waffleTime: initialWaffleTimeState,
waffleMetrics: initialWaffleOptionsState,
};
export const localReducer = combineReducers<LocalState>({
waffleFilter: waffleFilterReducer,
waffleTime: waffleTimeReducer,
waffleMetrics: waffleOptionsReducer,
});

View file

@ -1,26 +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 { globalizeSelectors } from '../../utils/typed_redux';
import { LocalState } from './reducer';
import { waffleFilterSelectors as innerWaffleFilterSelectors } from './waffle_filter';
import { waffleOptionsSelectors as innerWaffleOptionsSelectors } from './waffle_options';
import { waffleTimeSelectors as innerWaffleTimeSelectors } from './waffle_time';
export const waffleFilterSelectors = globalizeSelectors(
(state: LocalState) => state.waffleFilter,
innerWaffleFilterSelectors
);
export const waffleTimeSelectors = globalizeSelectors(
(state: LocalState) => state.waffleTime,
innerWaffleTimeSelectors
);
export const waffleOptionsSelectors = globalizeSelectors(
(state: LocalState) => state.waffleMetrics,
innerWaffleOptionsSelectors
);

View file

@ -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 actionCreatorFactory from 'typescript-fsa';
import { FilterQuery, SerializedFilterQuery } from './reducer';
const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_filter');
export const setWaffleFilterQueryDraft = actionCreator<FilterQuery>(
'SET_WAFFLE_FILTER_QUERY_DRAFT'
);
export const applyWaffleFilterQuery = actionCreator<SerializedFilterQuery>(
'APPLY_WAFFLE_FILTER_QUERY'
);

View file

@ -1,11 +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 * as waffleFilterActions from './actions';
import * as waffleFilterSelectors from './selectors';
export { waffleFilterActions, waffleFilterSelectors };
export * from './reducer';

View file

@ -1,43 +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 { reducerWithInitialState } from 'typescript-fsa-reducers/dist';
import { applyWaffleFilterQuery, setWaffleFilterQueryDraft } from './actions';
export interface KueryFilterQuery {
kind: 'kuery';
expression: string;
}
export type FilterQuery = KueryFilterQuery;
export interface SerializedFilterQuery {
query: FilterQuery | null;
serializedQuery: string | null;
}
export interface WaffleFilterState {
filterQuery: SerializedFilterQuery | null;
filterQueryDraft: KueryFilterQuery | null;
}
export const initialWaffleFilterState: WaffleFilterState = {
filterQuery: null,
filterQueryDraft: null,
};
export const waffleFilterReducer = reducerWithInitialState(initialWaffleFilterState)
.case(setWaffleFilterQueryDraft, (state, filterQueryDraft) => ({
...state,
filterQueryDraft,
}))
.case(applyWaffleFilterQuery, (state, filterQuery) => ({
...state,
filterQuery,
filterQueryDraft: filterQuery.query,
}))
.build();

View file

@ -1,33 +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 { createSelector } from 'reselect';
import { esKuery } from '../../../../../../../src/plugins/data/public';
import { WaffleFilterState } from './reducer';
export const selectWaffleFilterQuery = (state: WaffleFilterState) =>
state.filterQuery ? state.filterQuery.query : null;
export const selectWaffleFilterQueryAsJson = (state: WaffleFilterState) =>
state.filterQuery ? state.filterQuery.serializedQuery : null;
export const selectWaffleFilterQueryDraft = (state: WaffleFilterState) => state.filterQueryDraft;
export const selectIsWaffleFilterQueryDraftValid = createSelector(
selectWaffleFilterQueryDraft,
filterQueryDraft => {
if (filterQueryDraft && filterQueryDraft.kind === 'kuery') {
try {
esKuery.fromKueryExpression(filterQueryDraft.expression);
} catch (err) {
return false;
}
}
return true;
}
);

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 actionCreatorFactory from 'typescript-fsa';
import {
SnapshotGroupBy,
SnapshotMetricInput,
SnapshotCustomMetricInput,
} from '../../../../common/http_api/snapshot_api';
import { InventoryItemType } from '../../../../common/inventory_models/types';
import { InfraGroupByOptions, InfraWaffleMapBounds } from '../../../lib/lib';
const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_options');
export const changeMetric = actionCreator<SnapshotMetricInput>('CHANGE_METRIC');
export const changeGroupBy = actionCreator<SnapshotGroupBy>('CHANGE_GROUP_BY');
export const changeCustomOptions = actionCreator<InfraGroupByOptions[]>('CHANGE_CUSTOM_OPTIONS');
export const changeNodeType = actionCreator<InventoryItemType>('CHANGE_NODE_TYPE');
export const changeView = actionCreator<string>('CHANGE_VIEW');
export const changeBoundsOverride = actionCreator<InfraWaffleMapBounds>('CHANGE_BOUNDS_OVERRIDE');
export const changeAutoBounds = actionCreator<boolean>('CHANGE_AUTO_BOUNDS');
export const changeAccount = actionCreator<string>('CHANGE_ACCOUNT');
export const changeRegion = actionCreator<string>('CHANGE_REGION');
export const changeCustomMetrics = actionCreator<SnapshotCustomMetricInput[]>(
'CHANGE_CUSTOM_METRICS'
);

View file

@ -1,11 +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 * as waffleOptionsActions from './actions';
import * as waffleOptionsSelectors from './selector';
export { waffleOptionsActions, waffleOptionsSelectors };
export * from './reducer';

View file

@ -1,113 +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 { combineReducers } from 'redux';
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import {
SnapshotMetricInput,
SnapshotGroupBy,
SnapshotCustomMetricInput,
} from '../../../../common/http_api/snapshot_api';
import { InfraGroupByOptions, InfraWaffleMapBounds } from '../../../lib/lib';
import {
changeAutoBounds,
changeBoundsOverride,
changeCustomOptions,
changeGroupBy,
changeMetric,
changeNodeType,
changeView,
changeAccount,
changeRegion,
changeCustomMetrics,
} from './actions';
import { InventoryItemType } from '../../../../common/inventory_models/types';
export interface WaffleOptionsState {
metric: SnapshotMetricInput;
groupBy: SnapshotGroupBy;
nodeType: InventoryItemType;
view: string;
customOptions: InfraGroupByOptions[];
boundsOverride: InfraWaffleMapBounds;
autoBounds: boolean;
accountId: string;
region: string;
customMetrics: SnapshotCustomMetricInput[];
}
export const initialWaffleOptionsState: WaffleOptionsState = {
metric: { type: 'cpu' },
groupBy: [],
nodeType: 'host',
view: 'map',
customOptions: [],
boundsOverride: { max: 1, min: 0 },
autoBounds: true,
accountId: '',
region: '',
customMetrics: [],
};
const currentMetricReducer = reducerWithInitialState(initialWaffleOptionsState.metric).case(
changeMetric,
(current, target) => target
);
const currentCustomOptionsReducer = reducerWithInitialState(
initialWaffleOptionsState.customOptions
).case(changeCustomOptions, (current, target) => target);
const currentGroupByReducer = reducerWithInitialState(initialWaffleOptionsState.groupBy).case(
changeGroupBy,
(current, target) => target
);
const currentNodeTypeReducer = reducerWithInitialState(initialWaffleOptionsState.nodeType).case(
changeNodeType,
(current, target) => target
);
const currentViewReducer = reducerWithInitialState(initialWaffleOptionsState.view).case(
changeView,
(current, target) => target
);
const currentBoundsOverrideReducer = reducerWithInitialState(
initialWaffleOptionsState.boundsOverride
).case(changeBoundsOverride, (current, target) => target);
const currentAutoBoundsReducer = reducerWithInitialState(initialWaffleOptionsState.autoBounds).case(
changeAutoBounds,
(current, target) => target
);
const currentAccountIdReducer = reducerWithInitialState(initialWaffleOptionsState.accountId).case(
changeAccount,
(current, target) => target
);
const currentRegionReducer = reducerWithInitialState(initialWaffleOptionsState.region).case(
changeRegion,
(current, target) => target
);
const currentCustomMetricsReducer = reducerWithInitialState(
initialWaffleOptionsState.customMetrics
).case(changeCustomMetrics, (current, target) => target);
export const waffleOptionsReducer = combineReducers<WaffleOptionsState>({
metric: currentMetricReducer,
groupBy: currentGroupByReducer,
nodeType: currentNodeTypeReducer,
view: currentViewReducer,
customOptions: currentCustomOptionsReducer,
boundsOverride: currentBoundsOverrideReducer,
autoBounds: currentAutoBoundsReducer,
accountId: currentAccountIdReducer,
region: currentRegionReducer,
customMetrics: currentCustomMetricsReducer,
});

View file

@ -1,18 +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 { WaffleOptionsState } from './reducer';
export const selectMetric = (state: WaffleOptionsState) => state.metric;
export const selectGroupBy = (state: WaffleOptionsState) => state.groupBy;
export const selectCustomOptions = (state: WaffleOptionsState) => state.customOptions;
export const selectNodeType = (state: WaffleOptionsState) => state.nodeType;
export const selectView = (state: WaffleOptionsState) => state.view;
export const selectBoundsOverride = (state: WaffleOptionsState) => state.boundsOverride;
export const selectAutoBounds = (state: WaffleOptionsState) => state.autoBounds;
export const selectAccountId = (state: WaffleOptionsState) => state.accountId;
export const selectRegion = (state: WaffleOptionsState) => state.region;
export const selectCustomMetrics = (state: WaffleOptionsState) => state.customMetrics;

View file

@ -1,15 +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 actionCreatorFactory from 'typescript-fsa';
const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_time');
export const jumpToTime = actionCreator<number>('JUMP_TO_TIME');
export const startAutoReload = actionCreator('START_AUTO_RELOAD');
export const stopAutoReload = actionCreator('STOP_AUTO_RELOAD');

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 { Action } from 'redux';
import { Epic } from 'redux-observable';
import { timer } from 'rxjs';
import { exhaustMap, filter, map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { jumpToTime, startAutoReload, stopAutoReload } from './actions';
interface WaffleTimeEpicDependencies<State> {
selectWaffleTimeUpdatePolicyInterval: (state: State) => number | null;
}
export const createWaffleTimeEpic = <State>(): Epic<
Action,
Action,
State,
WaffleTimeEpicDependencies<State>
> => (action$, state$, { selectWaffleTimeUpdatePolicyInterval }) => {
const updateInterval$ = state$.pipe(map(selectWaffleTimeUpdatePolicyInterval), filter(isNotNull));
return action$.pipe(
filter(startAutoReload.match),
withLatestFrom(updateInterval$),
exhaustMap(([action, updateInterval]) =>
timer(0, updateInterval).pipe(
map(() => jumpToTime(Date.now())),
takeUntil(action$.pipe(filter(stopAutoReload.match)))
)
)
);
};
const isNotNull = <T>(value: T | null): value is T => value !== null;

View file

@ -1,12 +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 * as waffleTimeActions from './actions';
import * as waffleTimeSelectors from './selectors';
export { waffleTimeActions, waffleTimeSelectors };
export * from './epic';
export * from './reducer';

View file

@ -1,52 +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 { combineReducers } from 'redux';
import { reducerWithInitialState } from 'typescript-fsa-reducers/dist';
import { jumpToTime, startAutoReload, stopAutoReload } from './actions';
interface ManualTimeUpdatePolicy {
policy: 'manual';
}
interface IntervalTimeUpdatePolicy {
policy: 'interval';
interval: number;
}
type TimeUpdatePolicy = ManualTimeUpdatePolicy | IntervalTimeUpdatePolicy;
export interface WaffleTimeState {
currentTime: number;
updatePolicy: TimeUpdatePolicy;
}
export const initialWaffleTimeState: WaffleTimeState = {
currentTime: Date.now(),
updatePolicy: {
policy: 'manual',
},
};
const currentTimeReducer = reducerWithInitialState(initialWaffleTimeState.currentTime).case(
jumpToTime,
(currentTime, targetTime) => targetTime
);
const updatePolicyReducer = reducerWithInitialState(initialWaffleTimeState.updatePolicy)
.case(startAutoReload, () => ({
policy: 'interval',
interval: 5000,
}))
.case(stopAutoReload, () => ({
policy: 'manual',
}));
export const waffleTimeReducer = combineReducers<WaffleTimeState>({
currentTime: currentTimeReducer,
updatePolicy: updatePolicyReducer,
});

View file

@ -1,23 +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 { createSelector } from 'reselect';
import { WaffleTimeState } from './reducer';
export const selectCurrentTime = (state: WaffleTimeState) => state.currentTime;
export const selectIsAutoReloading = (state: WaffleTimeState) =>
state.updatePolicy.policy === 'interval';
export const selectTimeUpdatePolicyInterval = (state: WaffleTimeState) =>
state.updatePolicy.policy === 'interval' ? state.updatePolicy.interval : null;
export const selectCurrentTimeRange = createSelector(selectCurrentTime, currentTime => ({
from: currentTime - 1000 * 60 * 5,
interval: '1m',
to: currentTime,
}));

View file

@ -1,21 +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 { combineReducers } from 'redux';
import { initialLocalState, localReducer, LocalState } from './local';
export interface State {
local: LocalState;
}
export const initialState: State = {
local: initialLocalState,
};
export const reducer = combineReducers<State>({
local: localReducer,
});

View file

@ -1,22 +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 { globalizeSelectors } from '../utils/typed_redux';
import {
waffleFilterSelectors as localWaffleFilterSelectors,
waffleOptionsSelectors as localWaffleOptionsSelectors,
waffleTimeSelectors as localWaffleTimeSelectors,
} from './local';
import { State } from './reducer';
/**
* local selectors
*/
const selectLocal = (state: State) => state.local;
export const waffleFilterSelectors = globalizeSelectors(selectLocal, localWaffleFilterSelectors);
export const waffleTimeSelectors = globalizeSelectors(selectLocal, localWaffleTimeSelectors);
export const waffleOptionsSelectors = globalizeSelectors(selectLocal, localWaffleOptionsSelectors);

View file

@ -1,50 +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 { Action, applyMiddleware, compose, createStore as createBasicStore } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { createRootEpic, initialState, reducer, State, waffleTimeSelectors } from '.';
import { InfraApolloClient, InfraObservableApi } from '../lib/lib';
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose;
}
}
export interface StoreDependencies {
apolloClient: Observable<InfraApolloClient>;
observableApi: Observable<InfraObservableApi>;
}
export function createStore({ apolloClient, observableApi }: StoreDependencies) {
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const middlewareDependencies = {
postToApi$: observableApi.pipe(map(({ post }) => post)),
apolloClient$: apolloClient,
selectWaffleTimeUpdatePolicyInterval: waffleTimeSelectors.selectTimeUpdatePolicyInterval,
};
const epicMiddleware = createEpicMiddleware<Action, Action, State, typeof middlewareDependencies>(
{
dependencies: middlewareDependencies,
}
);
const store = createBasicStore(
reducer,
initialState,
composeEnhancers(applyMiddleware(epicMiddleware))
);
epicMiddleware.run(createRootEpic<State>());
return store;
}

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 { useSelector } from 'react-redux';
import React, { createContext } from 'react';
import { State, initialState } from '../store';
export const ReduxStateContext = createContext(initialState);
export const ReduxStateContextProvider = ({ children }: { children: JSX.Element }) => {
const state = useSelector((store: State) => store);
return <ReduxStateContext.Provider value={state}>{children}</ReduxStateContext.Provider>;
};

View file

@ -38,7 +38,7 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => {
},
async (requestContext, request, response) => {
try {
const { nodeId, nodeType, sourceId } = pipe(
const { nodeId, nodeType, sourceId, timeRange } = pipe(
InfraMetadataRequestRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
@ -52,7 +52,8 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => {
requestContext,
configuration,
nodeId,
nodeType
nodeType,
timeRange
);
const metricFeatures = pickFeatureName(metricsMetadata.buckets).map(
nameToFeature('metrics')
@ -62,7 +63,13 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => {
const cloudInstanceId = get<string>(info, 'cloud.instance.id');
const cloudMetricsMetadata = cloudInstanceId
? await getCloudMetricsMetadata(framework, requestContext, configuration, cloudInstanceId)
? await getCloudMetricsMetadata(
framework,
requestContext,
configuration,
cloudInstanceId,
timeRange
)
: { buckets: [] };
const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map(
nameToFeature('metrics')

View file

@ -21,7 +21,8 @@ export const getCloudMetricsMetadata = async (
framework: KibanaFramework,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
instanceId: string
instanceId: string,
timeRange: { from: number; to: number }
): Promise<InfraCloudMetricsAdapterResponse> => {
const metricQuery = {
allowNoIndices: true,
@ -30,7 +31,18 @@ export const getCloudMetricsMetadata = async (
body: {
query: {
bool: {
filter: [{ match: { 'cloud.instance.id': instanceId } }],
filter: [
{ match: { 'cloud.instance.id': instanceId } },
{
range: {
[sourceConfiguration.fields.timestamp]: {
gte: timeRange.from,
lte: timeRange.to,
format: 'epoch_millis',
},
},
},
],
should: CLOUD_METRICS_MODULES.map(module => ({ match: { 'event.module': module } })),
},
},

View file

@ -26,7 +26,8 @@ export const getMetricMetadata = async (
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
nodeId: string,
nodeType: InventoryItemType
nodeType: InventoryItemType,
timeRange: { from: number; to: number }
): Promise<InfraMetricsAdapterResponse> => {
const fields = findInventoryFields(nodeType, sourceConfiguration.fields);
const metricQuery = {
@ -41,6 +42,15 @@ export const getMetricMetadata = async (
{
match: { [fields.id]: nodeId },
},
{
range: {
[sourceConfiguration.fields.timestamp]: {
gte: timeRange.from,
lte: timeRange.to,
format: 'epoch_millis',
},
},
},
],
},
},

View file

@ -12,6 +12,28 @@ import {
} from '../../../../plugins/infra/common/http_api/metadata_api';
import { FtrProviderContext } from '../../ftr_provider_context';
import { DATES } from './constants';
const timeRange700 = {
from: DATES['7.0.0'].hosts.min,
to: DATES[`7.0.0`].hosts.max,
};
const timeRange660 = {
from: DATES['6.6.0'].docker.min,
to: DATES[`6.6.0`].docker.max,
};
const timeRange800withAws = {
from: DATES['8.0.0'].logs_and_metrics_with_aws.min,
to: DATES[`8.0.0`].logs_and_metrics_with_aws.max,
};
const timeRange800 = {
from: DATES['8.0.0'].logs_and_metrics.min,
to: DATES[`8.0.0`].logs_and_metrics.max,
};
export default function({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
@ -34,6 +56,7 @@ export default function({ getService }: FtrProviderContext) {
sourceId: 'default',
nodeId: 'demo-stack-mysql-01',
nodeType: InfraNodeType.host,
timeRange: timeRange700,
});
if (metadata) {
expect(metadata.features.length).to.be(12);
@ -53,6 +76,7 @@ export default function({ getService }: FtrProviderContext) {
sourceId: 'default',
nodeId: '631f36a845514442b93c3fdd2dc91bcd8feb680b8ac5832c7fb8fdc167bb938e',
nodeType: InfraNodeType.container,
timeRange: timeRange660,
});
if (metadata) {
expect(metadata.features.length).to.be(10);
@ -74,6 +98,7 @@ export default function({ getService }: FtrProviderContext) {
sourceId: 'default',
nodeId: 'gke-observability-8--observability-8--bc1afd95-f0zc',
nodeType: InfraNodeType.host,
timeRange: timeRange800withAws,
});
if (metadata) {
expect(metadata.features.length).to.be(58);
@ -114,6 +139,7 @@ export default function({ getService }: FtrProviderContext) {
sourceId: 'default',
nodeId: 'ip-172-31-47-9.us-east-2.compute.internal',
nodeType: InfraNodeType.host,
timeRange: timeRange800withAws,
});
if (metadata) {
expect(metadata.features.length).to.be(19);
@ -155,6 +181,7 @@ export default function({ getService }: FtrProviderContext) {
sourceId: 'default',
nodeId: '14887487-99f8-11e9-9a96-42010a84004d',
nodeType: InfraNodeType.pod,
timeRange: timeRange800withAws,
});
if (metadata) {
expect(metadata.features.length).to.be(29);
@ -200,6 +227,7 @@ export default function({ getService }: FtrProviderContext) {
sourceId: 'default',
nodeId: 'c74b04834c6d7cc1800c3afbe31d0c8c0c267f06e9eb45c2b0c2df3e6cee40c5',
nodeType: InfraNodeType.container,
timeRange: timeRange800withAws,
});
if (metadata) {
expect(metadata.features.length).to.be(26);
@ -251,6 +279,7 @@ export default function({ getService }: FtrProviderContext) {
sourceId: 'default',
nodeId: 'gke-observability-8--observability-8--bc1afd95-f0zc',
nodeType: 'host',
timeRange: timeRange800,
});
if (metadata) {
expect(
@ -265,6 +294,7 @@ export default function({ getService }: FtrProviderContext) {
sourceId: 'default',
nodeId: 'c1031331-9ae0-11e9-9a96-42010a84004d',
nodeType: 'pod',
timeRange: timeRange800,
});
if (metadata) {
expect(

View file

@ -5,7 +5,7 @@
*/
declare module 'rison-node' {
export type RisonValue = null | boolean | number | string | RisonObject | RisonArray;
export type RisonValue = undefined | null | boolean | number | string | RisonObject | RisonArray;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface RisonArray extends Array<RisonValue> {}