[Observability] Load hasData call asynchronously (#80644)
* obs perf * fixing unit tests * fixing ts issues * fixing empty state * addressing pr comments * addressing pr comments * fixing TS issue * fixing some stuff * refactoring * fixing ts issues and unit tests * addressing PR comments * fixing TS issues * fixing eslint issue Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e3ca8a928d
commit
ac73b6a5b4
|
@ -8,6 +8,7 @@ import {
|
||||||
FetchDataParams,
|
FetchDataParams,
|
||||||
HasDataParams,
|
HasDataParams,
|
||||||
UxFetchDataResponse,
|
UxFetchDataResponse,
|
||||||
|
UXHasDataResponse,
|
||||||
} from '../../../../../observability/public/';
|
} from '../../../../../observability/public/';
|
||||||
import { callApmApi } from '../../../services/rest/createCallApmApi';
|
import { callApmApi } from '../../../services/rest/createCallApmApi';
|
||||||
|
|
||||||
|
@ -35,7 +36,9 @@ export const fetchUxOverviewDate = async ({
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function hasRumData({ absoluteTime }: HasDataParams) {
|
export async function hasRumData({
|
||||||
|
absoluteTime,
|
||||||
|
}: HasDataParams): Promise<UXHasDataResponse> {
|
||||||
return await callApmApi({
|
return await callApmApi({
|
||||||
endpoint: 'GET /api/apm/observability_overview/has_rum_data',
|
endpoint: 'GET /api/apm/observability_overview/has_rum_data',
|
||||||
params: {
|
params: {
|
||||||
|
|
|
@ -54,6 +54,6 @@ export async function hasRumData({ setup }: { setup: Setup & SetupTimeRange }) {
|
||||||
response.aggregations?.services?.mostTraffic?.buckets?.[0]?.key,
|
response.aggregations?.services?.mostTraffic?.buckets?.[0]?.key,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return { hasData: false, serviceName: undefined };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,7 @@
|
||||||
|
|
||||||
import { encode } from 'rison-node';
|
import { encode } from 'rison-node';
|
||||||
import { SearchResponse } from 'elasticsearch';
|
import { SearchResponse } from 'elasticsearch';
|
||||||
import {
|
import { FetchData, FetchDataParams, LogsFetchDataResponse } from '../../../observability/public';
|
||||||
FetchData,
|
|
||||||
FetchDataParams,
|
|
||||||
HasData,
|
|
||||||
LogsFetchDataResponse,
|
|
||||||
} from '../../../observability/public';
|
|
||||||
import { DEFAULT_SOURCE_ID } from '../../common/constants';
|
import { DEFAULT_SOURCE_ID } from '../../common/constants';
|
||||||
import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration';
|
import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration';
|
||||||
import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status';
|
import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status';
|
||||||
|
@ -38,9 +33,7 @@ interface LogParams {
|
||||||
|
|
||||||
type StatsAndSeries = Pick<LogsFetchDataResponse, 'stats' | 'series'>;
|
type StatsAndSeries = Pick<LogsFetchDataResponse, 'stats' | 'series'>;
|
||||||
|
|
||||||
export function getLogsHasDataFetcher(
|
export function getLogsHasDataFetcher(getStartServices: InfraClientCoreSetup['getStartServices']) {
|
||||||
getStartServices: InfraClientCoreSetup['getStartServices']
|
|
||||||
): HasData {
|
|
||||||
return async () => {
|
return async () => {
|
||||||
const [core] = await getStartServices();
|
const [core] = await getStartServices();
|
||||||
const sourceStatus = await callFetchLogSourceStatusAPI(DEFAULT_SOURCE_ID, core.http.fetch);
|
const sourceStatus = await callFetchLogSourceStatusAPI(DEFAULT_SOURCE_ID, core.http.fetch);
|
||||||
|
|
|
@ -11,9 +11,25 @@ import { ObservabilityPluginSetupDeps } from '../plugin';
|
||||||
import { renderApp } from './';
|
import { renderApp } from './';
|
||||||
|
|
||||||
describe('renderApp', () => {
|
describe('renderApp', () => {
|
||||||
|
const originalConsole = global.console;
|
||||||
|
beforeAll(() => {
|
||||||
|
// mocks console to avoid poluting the test output
|
||||||
|
global.console = ({ error: jest.fn() } as unknown) as typeof console;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
global.console = originalConsole;
|
||||||
|
});
|
||||||
it('renders', async () => {
|
it('renders', async () => {
|
||||||
const plugins = ({
|
const plugins = ({
|
||||||
usageCollection: { reportUiStats: () => {} },
|
usageCollection: { reportUiStats: () => {} },
|
||||||
|
data: {
|
||||||
|
query: {
|
||||||
|
timefilter: {
|
||||||
|
timefilter: { setTime: jest.fn(), getTime: jest.fn().mockImplementation(() => ({})) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
} as unknown) as ObservabilityPluginSetupDeps;
|
} as unknown) as ObservabilityPluginSetupDeps;
|
||||||
const core = ({
|
const core = ({
|
||||||
application: { currentAppId$: new Observable(), navigateToUrl: () => {} },
|
application: { currentAppId$: new Observable(), navigateToUrl: () => {} },
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { PluginContext } from '../context/plugin_context';
|
||||||
import { usePluginContext } from '../hooks/use_plugin_context';
|
import { usePluginContext } from '../hooks/use_plugin_context';
|
||||||
import { useRouteParams } from '../hooks/use_route_params';
|
import { useRouteParams } from '../hooks/use_route_params';
|
||||||
import { ObservabilityPluginSetupDeps } from '../plugin';
|
import { ObservabilityPluginSetupDeps } from '../plugin';
|
||||||
|
import { HasDataContextProvider } from '../context/has_data_context';
|
||||||
import { Breadcrumbs, routes } from '../routes';
|
import { Breadcrumbs, routes } from '../routes';
|
||||||
|
|
||||||
const observabilityLabelBreadcrumb = {
|
const observabilityLabelBreadcrumb = {
|
||||||
|
@ -46,8 +47,8 @@ function App() {
|
||||||
core.chrome.docTitle.change(getTitleFromBreadCrumbs(breadcrumb));
|
core.chrome.docTitle.change(getTitleFromBreadCrumbs(breadcrumb));
|
||||||
}, [core, breadcrumb]);
|
}, [core, breadcrumb]);
|
||||||
|
|
||||||
const { query, path: pathParams } = useRouteParams(route.params);
|
const params = useRouteParams(path);
|
||||||
return route.handler({ query, path: pathParams });
|
return route.handler(params);
|
||||||
};
|
};
|
||||||
return <Route key={path} path={path} exact={true} component={Wrapper} />;
|
return <Route key={path} path={path} exact={true} component={Wrapper} />;
|
||||||
})}
|
})}
|
||||||
|
@ -79,7 +80,9 @@ export const renderApp = (
|
||||||
<EuiThemeProvider darkMode={isDarkMode}>
|
<EuiThemeProvider darkMode={isDarkMode}>
|
||||||
<i18nCore.Context>
|
<i18nCore.Context>
|
||||||
<RedirectAppLinks application={core.application}>
|
<RedirectAppLinks application={core.application}>
|
||||||
<App />
|
<HasDataContextProvider>
|
||||||
|
<App />
|
||||||
|
</HasDataContextProvider>
|
||||||
</RedirectAppLinks>
|
</RedirectAppLinks>
|
||||||
</i18nCore.Context>
|
</i18nCore.Context>
|
||||||
</EuiThemeProvider>
|
</EuiThemeProvider>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ISection } from '../../../typings/section';
|
import { ISection } from '../../../typings/section';
|
||||||
import { render } from '../../../utils/test_helper';
|
import { render } from '../../../utils/test_helper';
|
||||||
import { EmptySection } from './';
|
import { EmptySection } from './empty_section';
|
||||||
|
|
||||||
describe('EmptySection', () => {
|
describe('EmptySection', () => {
|
||||||
it('renders without action button', () => {
|
it('renders without action button', () => {
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* 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 { EuiFlexGrid, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { ThemeContext } from 'styled-components';
|
||||||
|
import { Alert } from '../../../../../alerts/common';
|
||||||
|
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||||
|
import { useHasData } from '../../../hooks/use_has_data';
|
||||||
|
import { usePluginContext } from '../../../hooks/use_plugin_context';
|
||||||
|
import { getEmptySections } from '../../../pages/overview/empty_section';
|
||||||
|
import { UXHasDataResponse } from '../../../typings';
|
||||||
|
import { EmptySection } from './empty_section';
|
||||||
|
|
||||||
|
export function EmptySections() {
|
||||||
|
const { core } = usePluginContext();
|
||||||
|
const theme = useContext(ThemeContext);
|
||||||
|
const { hasData } = useHasData();
|
||||||
|
|
||||||
|
const appEmptySections = getEmptySections({ core }).filter(({ id }) => {
|
||||||
|
if (id === 'alert') {
|
||||||
|
const { status, hasData: alerts } = hasData.alert || {};
|
||||||
|
return (
|
||||||
|
status === FETCH_STATUS.FAILURE ||
|
||||||
|
(status === FETCH_STATUS.SUCCESS && (alerts as Alert[]).length === 0)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const app = hasData[id];
|
||||||
|
if (app) {
|
||||||
|
const _hasData = id === 'ux' ? (app.hasData as UXHasDataResponse)?.hasData : app.hasData;
|
||||||
|
return app.status === FETCH_STATUS.FAILURE || !_hasData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<EuiFlexGrid
|
||||||
|
columns={
|
||||||
|
// when more than 2 empty sections are available show them on 2 columns, otherwise 1
|
||||||
|
appEmptySections.length > 2 ? 2 : 1
|
||||||
|
}
|
||||||
|
gutterSize="s"
|
||||||
|
>
|
||||||
|
{appEmptySections.map((app) => {
|
||||||
|
return (
|
||||||
|
<EuiFlexItem
|
||||||
|
key={app.id}
|
||||||
|
style={{
|
||||||
|
border: `1px dashed ${theme.eui.euiBorderColor}`,
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EmptySection section={app} />
|
||||||
|
</EuiFlexItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</EuiFlexGrid>
|
||||||
|
</EuiFlexItem>
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,25 +8,59 @@ import * as fetcherHook from '../../../../hooks/use_fetcher';
|
||||||
import { render } from '../../../../utils/test_helper';
|
import { render } from '../../../../utils/test_helper';
|
||||||
import { APMSection } from './';
|
import { APMSection } from './';
|
||||||
import { response } from './mock_data/apm.mock';
|
import { response } from './mock_data/apm.mock';
|
||||||
import moment from 'moment';
|
import * as hasDataHook from '../../../../hooks/use_has_data';
|
||||||
|
import * as pluginContext from '../../../../hooks/use_plugin_context';
|
||||||
|
import { HasDataContextValue } from '../../../../context/has_data_context';
|
||||||
|
import { AppMountParameters, CoreStart } from 'kibana/public';
|
||||||
|
import { ObservabilityPluginSetupDeps } from '../../../../plugin';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
useLocation: () => ({
|
||||||
|
pathname: '/observability/overview/',
|
||||||
|
search: '',
|
||||||
|
}),
|
||||||
|
useHistory: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('APMSection', () => {
|
describe('APMSection', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(hasDataHook, 'useHasData').mockReturnValue({
|
||||||
|
hasData: {
|
||||||
|
apm: {
|
||||||
|
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||||
|
hasData: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as HasDataContextValue);
|
||||||
|
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
|
||||||
|
core: ({
|
||||||
|
uiSettings: { get: jest.fn() },
|
||||||
|
http: { basePath: { prepend: jest.fn() } },
|
||||||
|
} as unknown) as CoreStart,
|
||||||
|
appMountParameters: {} as AppMountParameters,
|
||||||
|
plugins: ({
|
||||||
|
data: {
|
||||||
|
query: {
|
||||||
|
timefilter: {
|
||||||
|
timefilter: {
|
||||||
|
getTime: jest.fn().mockImplementation(() => ({
|
||||||
|
from: '2020-10-08T06:00:00.000Z',
|
||||||
|
to: '2020-10-08T07:00:00.000Z',
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown) as ObservabilityPluginSetupDeps,
|
||||||
|
}));
|
||||||
|
});
|
||||||
it('renders with transaction series and stats', () => {
|
it('renders with transaction series and stats', () => {
|
||||||
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
||||||
data: response,
|
data: response,
|
||||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||||
refetch: jest.fn(),
|
refetch: jest.fn(),
|
||||||
});
|
});
|
||||||
const { getByText, queryAllByTestId } = render(
|
const { getByText, queryAllByTestId } = render(<APMSection bucketSize="60s" />);
|
||||||
<APMSection
|
|
||||||
absoluteTime={{
|
|
||||||
start: moment('2020-06-29T11:38:23.747Z').valueOf(),
|
|
||||||
end: moment('2020-06-29T12:08:23.748Z').valueOf(),
|
|
||||||
}}
|
|
||||||
relativeTime={{ start: 'now-15m', end: 'now' }}
|
|
||||||
bucketSize="60s"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText('APM')).toBeInTheDocument();
|
expect(getByText('APM')).toBeInTheDocument();
|
||||||
expect(getByText('View in app')).toBeInTheDocument();
|
expect(getByText('View in app')).toBeInTheDocument();
|
||||||
|
@ -40,16 +74,7 @@ describe('APMSection', () => {
|
||||||
status: fetcherHook.FETCH_STATUS.LOADING,
|
status: fetcherHook.FETCH_STATUS.LOADING,
|
||||||
refetch: jest.fn(),
|
refetch: jest.fn(),
|
||||||
});
|
});
|
||||||
const { getByText, queryAllByText, getByTestId } = render(
|
const { getByText, queryAllByText, getByTestId } = render(<APMSection bucketSize="60s" />);
|
||||||
<APMSection
|
|
||||||
absoluteTime={{
|
|
||||||
start: moment('2020-06-29T11:38:23.747Z').valueOf(),
|
|
||||||
end: moment('2020-06-29T12:08:23.748Z').valueOf(),
|
|
||||||
}}
|
|
||||||
relativeTime={{ start: 'now-15m', end: 'now' }}
|
|
||||||
bucketSize="60s"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText('APM')).toBeInTheDocument();
|
expect(getByText('APM')).toBeInTheDocument();
|
||||||
expect(getByTestId('loading')).toBeInTheDocument();
|
expect(getByTestId('loading')).toBeInTheDocument();
|
||||||
|
|
|
@ -12,17 +12,17 @@ import moment from 'moment';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { ThemeContext } from 'styled-components';
|
import { ThemeContext } from 'styled-components';
|
||||||
|
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||||
import { SectionContainer } from '../';
|
import { SectionContainer } from '../';
|
||||||
import { getDataHandler } from '../../../../data_handler';
|
import { getDataHandler } from '../../../../data_handler';
|
||||||
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
||||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||||
|
import { useHasData } from '../../../../hooks/use_has_data';
|
||||||
import { ChartContainer } from '../../chart_container';
|
import { ChartContainer } from '../../chart_container';
|
||||||
import { StyledStat } from '../../styled_stat';
|
import { StyledStat } from '../../styled_stat';
|
||||||
import { onBrushEnd } from '../helper';
|
import { onBrushEnd } from '../helper';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
absoluteTime: { start?: number; end?: number };
|
|
||||||
relativeTime: { start: string; end: string };
|
|
||||||
bucketSize?: string;
|
bucketSize?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,25 +30,36 @@ function formatTpm(value?: number) {
|
||||||
return numeral(value).format('0.00a');
|
return numeral(value).format('0.00a');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function APMSection({ absoluteTime, relativeTime, bucketSize }: Props) {
|
export function APMSection({ bucketSize }: Props) {
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useContext(ThemeContext);
|
||||||
|
const chartTheme = useChartTheme();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { forceUpdate, hasData } = useHasData();
|
||||||
|
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
|
||||||
|
|
||||||
const { start, end } = absoluteTime;
|
const { data, status } = useFetcher(
|
||||||
const { data, status } = useFetcher(() => {
|
() => {
|
||||||
if (start && end && bucketSize) {
|
if (bucketSize) {
|
||||||
return getDataHandler('apm')?.fetchData({
|
return getDataHandler('apm')?.fetchData({
|
||||||
absoluteTime: { start, end },
|
absoluteTime: { start: absoluteStart, end: absoluteEnd },
|
||||||
relativeTime,
|
relativeTime: { start: relativeStart, end: relativeEnd },
|
||||||
bucketSize,
|
bucketSize,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [start, end, bucketSize, relativeTime]);
|
},
|
||||||
|
// Absolute times shouldn't be used here, since it would refetch on every render
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[bucketSize, relativeStart, relativeEnd, forceUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasData.apm?.hasData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const { appLink, stats, series } = data || {};
|
const { appLink, stats, series } = data || {};
|
||||||
|
|
||||||
const min = moment.utc(absoluteTime.start).valueOf();
|
const min = moment.utc(absoluteStart).valueOf();
|
||||||
const max = moment.utc(absoluteTime.end).valueOf();
|
const max = moment.utc(absoluteEnd).valueOf();
|
||||||
|
|
||||||
const formatter = niceTimeFormatter([min, max]);
|
const formatter = niceTimeFormatter([min, max]);
|
||||||
|
|
||||||
|
@ -93,7 +104,7 @@ export function APMSection({ absoluteTime, relativeTime, bucketSize }: Props) {
|
||||||
<ChartContainer isInitialLoad={isLoading && !data}>
|
<ChartContainer isInitialLoad={isLoading && !data}>
|
||||||
<Settings
|
<Settings
|
||||||
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
|
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
|
||||||
theme={useChartTheme()}
|
theme={chartTheme}
|
||||||
showLegend={false}
|
showLegend={false}
|
||||||
xDomain={{ min, max }}
|
xDomain={{ min, max }}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,19 +5,19 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts';
|
import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts';
|
||||||
import { EuiFlexGroup, EuiFlexItem, euiPaletteColorBlind } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem, euiPaletteColorBlind, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||||
import numeral from '@elastic/numeral';
|
import numeral from '@elastic/numeral';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { EuiTitle } from '@elastic/eui';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { EuiSpacer } from '@elastic/eui';
|
|
||||||
import { SectionContainer } from '../';
|
import { SectionContainer } from '../';
|
||||||
import { getDataHandler } from '../../../../data_handler';
|
import { getDataHandler } from '../../../../data_handler';
|
||||||
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
||||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||||
|
import { useHasData } from '../../../../hooks/use_has_data';
|
||||||
|
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||||
import { LogsFetchDataResponse } from '../../../../typings';
|
import { LogsFetchDataResponse } from '../../../../typings';
|
||||||
import { formatStatValue } from '../../../../utils/format_stat_value';
|
import { formatStatValue } from '../../../../utils/format_stat_value';
|
||||||
import { ChartContainer } from '../../chart_container';
|
import { ChartContainer } from '../../chart_container';
|
||||||
|
@ -25,8 +25,6 @@ import { StyledStat } from '../../styled_stat';
|
||||||
import { onBrushEnd } from '../helper';
|
import { onBrushEnd } from '../helper';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
absoluteTime: { start?: number; end?: number };
|
|
||||||
relativeTime: { start: string; end: string };
|
|
||||||
bucketSize?: string;
|
bucketSize?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,22 +43,33 @@ function getColorPerItem(series?: LogsFetchDataResponse['series']) {
|
||||||
return colorsPerItem;
|
return colorsPerItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LogsSection({ absoluteTime, relativeTime, bucketSize }: Props) {
|
export function LogsSection({ bucketSize }: Props) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const chartTheme = useChartTheme();
|
||||||
|
const { forceUpdate, hasData } = useHasData();
|
||||||
|
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
|
||||||
|
|
||||||
const { start, end } = absoluteTime;
|
const { data, status } = useFetcher(
|
||||||
const { data, status } = useFetcher(() => {
|
() => {
|
||||||
if (start && end && bucketSize) {
|
if (bucketSize) {
|
||||||
return getDataHandler('infra_logs')?.fetchData({
|
return getDataHandler('infra_logs')?.fetchData({
|
||||||
absoluteTime: { start, end },
|
absoluteTime: { start: absoluteStart, end: absoluteEnd },
|
||||||
relativeTime,
|
relativeTime: { start: relativeStart, end: relativeEnd },
|
||||||
bucketSize,
|
bucketSize,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [start, end, bucketSize, relativeTime]);
|
},
|
||||||
|
// Absolute times shouldn't be used here, since it would refetch on every render
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[bucketSize, relativeStart, relativeEnd, forceUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
const min = moment.utc(absoluteTime.start).valueOf();
|
if (!hasData.infra_logs?.hasData) {
|
||||||
const max = moment.utc(absoluteTime.end).valueOf();
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = moment.utc(absoluteStart).valueOf();
|
||||||
|
const max = moment.utc(absoluteEnd).valueOf();
|
||||||
|
|
||||||
const formatter = niceTimeFormatter([min, max]);
|
const formatter = niceTimeFormatter([min, max]);
|
||||||
|
|
||||||
|
@ -115,7 +124,7 @@ export function LogsSection({ absoluteTime, relativeTime, bucketSize }: Props) {
|
||||||
<ChartContainer isInitialLoad={isLoading && !data}>
|
<ChartContainer isInitialLoad={isLoading && !data}>
|
||||||
<Settings
|
<Settings
|
||||||
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
|
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
|
||||||
theme={useChartTheme()}
|
theme={chartTheme}
|
||||||
showLegend
|
showLegend
|
||||||
legendPosition={Position.Right}
|
legendPosition={Position.Right}
|
||||||
xDomain={{ min, max }}
|
xDomain={{ min, max }}
|
||||||
|
|
|
@ -13,13 +13,13 @@ import { SectionContainer } from '../';
|
||||||
import { getDataHandler } from '../../../../data_handler';
|
import { getDataHandler } from '../../../../data_handler';
|
||||||
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
||||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||||
|
import { useHasData } from '../../../../hooks/use_has_data';
|
||||||
|
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||||
import { Series } from '../../../../typings';
|
import { Series } from '../../../../typings';
|
||||||
import { ChartContainer } from '../../chart_container';
|
import { ChartContainer } from '../../chart_container';
|
||||||
import { StyledStat } from '../../styled_stat';
|
import { StyledStat } from '../../styled_stat';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
absoluteTime: { start?: number; end?: number };
|
|
||||||
relativeTime: { start: string; end: string };
|
|
||||||
bucketSize?: string;
|
bucketSize?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,19 +46,29 @@ const StyledProgress = styled.div<{ color?: string }>`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function MetricsSection({ absoluteTime, relativeTime, bucketSize }: Props) {
|
export function MetricsSection({ bucketSize }: Props) {
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useContext(ThemeContext);
|
||||||
|
const { forceUpdate, hasData } = useHasData();
|
||||||
|
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
|
||||||
|
|
||||||
const { start, end } = absoluteTime;
|
const { data, status } = useFetcher(
|
||||||
const { data, status } = useFetcher(() => {
|
() => {
|
||||||
if (start && end && bucketSize) {
|
if (bucketSize) {
|
||||||
return getDataHandler('infra_metrics')?.fetchData({
|
return getDataHandler('infra_metrics')?.fetchData({
|
||||||
absoluteTime: { start, end },
|
absoluteTime: { start: absoluteStart, end: absoluteEnd },
|
||||||
relativeTime,
|
relativeTime: { start: relativeStart, end: relativeEnd },
|
||||||
bucketSize,
|
bucketSize,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [start, end, bucketSize, relativeTime]);
|
},
|
||||||
|
// Absolute times shouldn't be used here, since it would refetch on every render
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[bucketSize, relativeStart, relativeEnd, forceUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasData.infra_metrics?.hasData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const isLoading = status === FETCH_STATUS.LOADING;
|
const isLoading = status === FETCH_STATUS.LOADING;
|
||||||
|
|
||||||
|
|
|
@ -24,34 +24,45 @@ import { SectionContainer } from '../';
|
||||||
import { getDataHandler } from '../../../../data_handler';
|
import { getDataHandler } from '../../../../data_handler';
|
||||||
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
||||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||||
|
import { useHasData } from '../../../../hooks/use_has_data';
|
||||||
|
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||||
import { Series } from '../../../../typings';
|
import { Series } from '../../../../typings';
|
||||||
import { ChartContainer } from '../../chart_container';
|
import { ChartContainer } from '../../chart_container';
|
||||||
import { StyledStat } from '../../styled_stat';
|
import { StyledStat } from '../../styled_stat';
|
||||||
import { onBrushEnd } from '../helper';
|
import { onBrushEnd } from '../helper';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
absoluteTime: { start?: number; end?: number };
|
|
||||||
relativeTime: { start: string; end: string };
|
|
||||||
bucketSize?: string;
|
bucketSize?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UptimeSection({ absoluteTime, relativeTime, bucketSize }: Props) {
|
export function UptimeSection({ bucketSize }: Props) {
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useContext(ThemeContext);
|
||||||
|
const chartTheme = useChartTheme();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { forceUpdate, hasData } = useHasData();
|
||||||
|
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
|
||||||
|
|
||||||
const { start, end } = absoluteTime;
|
const { data, status } = useFetcher(
|
||||||
const { data, status } = useFetcher(() => {
|
() => {
|
||||||
if (start && end && bucketSize) {
|
if (bucketSize) {
|
||||||
return getDataHandler('uptime')?.fetchData({
|
return getDataHandler('uptime')?.fetchData({
|
||||||
absoluteTime: { start, end },
|
absoluteTime: { start: absoluteStart, end: absoluteEnd },
|
||||||
relativeTime,
|
relativeTime: { start: relativeStart, end: relativeEnd },
|
||||||
bucketSize,
|
bucketSize,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [start, end, bucketSize, relativeTime]);
|
},
|
||||||
|
// Absolute times shouldn't be used here, since it would refetch on every render
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[bucketSize, relativeStart, relativeEnd, forceUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
const min = moment.utc(absoluteTime.start).valueOf();
|
if (!hasData.uptime?.hasData) {
|
||||||
const max = moment.utc(absoluteTime.end).valueOf();
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = moment.utc(absoluteStart).valueOf();
|
||||||
|
const max = moment.utc(absoluteEnd).valueOf();
|
||||||
|
|
||||||
const formatter = niceTimeFormatter([min, max]);
|
const formatter = niceTimeFormatter([min, max]);
|
||||||
|
|
||||||
|
@ -112,7 +123,7 @@ export function UptimeSection({ absoluteTime, relativeTime, bucketSize }: Props)
|
||||||
<ChartContainer isInitialLoad={isLoading && !data}>
|
<ChartContainer isInitialLoad={isLoading && !data}>
|
||||||
<Settings
|
<Settings
|
||||||
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
|
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
|
||||||
theme={useChartTheme()}
|
theme={chartTheme}
|
||||||
showLegend={false}
|
showLegend={false}
|
||||||
legendPosition={Position.Right}
|
legendPosition={Position.Right}
|
||||||
xDomain={{ min, max }}
|
xDomain={{ min, max }}
|
||||||
|
|
|
@ -3,31 +3,63 @@
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
import { AppMountParameters, CoreStart } from 'kibana/public';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import moment from 'moment';
|
import { HasDataContextValue } from '../../../../context/has_data_context';
|
||||||
import * as fetcherHook from '../../../../hooks/use_fetcher';
|
import * as fetcherHook from '../../../../hooks/use_fetcher';
|
||||||
|
import * as hasDataHook from '../../../../hooks/use_has_data';
|
||||||
|
import * as pluginContext from '../../../../hooks/use_plugin_context';
|
||||||
|
import { ObservabilityPluginSetupDeps } from '../../../../plugin';
|
||||||
import { render } from '../../../../utils/test_helper';
|
import { render } from '../../../../utils/test_helper';
|
||||||
import { UXSection } from './';
|
import { UXSection } from './';
|
||||||
import { response } from './mock_data/ux.mock';
|
import { response } from './mock_data/ux.mock';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
useLocation: () => ({
|
||||||
|
pathname: '/observability/overview/',
|
||||||
|
search: '',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('UXSection', () => {
|
describe('UXSection', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(hasDataHook, 'useHasData').mockReturnValue({
|
||||||
|
hasData: {
|
||||||
|
ux: {
|
||||||
|
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||||
|
hasData: { hasData: true, serviceName: 'elastic-co-frontend' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as HasDataContextValue);
|
||||||
|
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
|
||||||
|
core: ({
|
||||||
|
uiSettings: { get: jest.fn() },
|
||||||
|
http: { basePath: { prepend: jest.fn() } },
|
||||||
|
} as unknown) as CoreStart,
|
||||||
|
appMountParameters: {} as AppMountParameters,
|
||||||
|
plugins: ({
|
||||||
|
data: {
|
||||||
|
query: {
|
||||||
|
timefilter: {
|
||||||
|
timefilter: {
|
||||||
|
getTime: jest.fn().mockImplementation(() => ({
|
||||||
|
from: '2020-10-08T06:00:00.000Z',
|
||||||
|
to: '2020-10-08T07:00:00.000Z',
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown) as ObservabilityPluginSetupDeps,
|
||||||
|
}));
|
||||||
|
});
|
||||||
it('renders with core web vitals', () => {
|
it('renders with core web vitals', () => {
|
||||||
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
||||||
data: response,
|
data: response,
|
||||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||||
refetch: jest.fn(),
|
refetch: jest.fn(),
|
||||||
});
|
});
|
||||||
const { getByText, getAllByText } = render(
|
const { getByText, getAllByText } = render(<UXSection bucketSize="60s" />);
|
||||||
<UXSection
|
|
||||||
absoluteTime={{
|
|
||||||
start: moment('2020-06-29T11:38:23.747Z').valueOf(),
|
|
||||||
end: moment('2020-06-29T12:08:23.748Z').valueOf(),
|
|
||||||
}}
|
|
||||||
relativeTime={{ start: 'now-15m', end: 'now' }}
|
|
||||||
bucketSize="60s"
|
|
||||||
serviceName="elastic-co-frontend"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText('User Experience')).toBeInTheDocument();
|
expect(getByText('User Experience')).toBeInTheDocument();
|
||||||
expect(getByText('View in app')).toBeInTheDocument();
|
expect(getByText('View in app')).toBeInTheDocument();
|
||||||
|
@ -59,17 +91,7 @@ describe('UXSection', () => {
|
||||||
status: fetcherHook.FETCH_STATUS.LOADING,
|
status: fetcherHook.FETCH_STATUS.LOADING,
|
||||||
refetch: jest.fn(),
|
refetch: jest.fn(),
|
||||||
});
|
});
|
||||||
const { getByText, queryAllByText, getAllByText } = render(
|
const { getByText, queryAllByText, getAllByText } = render(<UXSection bucketSize="60s" />);
|
||||||
<UXSection
|
|
||||||
absoluteTime={{
|
|
||||||
start: moment('2020-06-29T11:38:23.747Z').valueOf(),
|
|
||||||
end: moment('2020-06-29T12:08:23.748Z').valueOf(),
|
|
||||||
}}
|
|
||||||
relativeTime={{ start: 'now-15m', end: 'now' }}
|
|
||||||
bucketSize="60s"
|
|
||||||
serviceName="elastic-co-frontend"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText('User Experience')).toBeInTheDocument();
|
expect(getByText('User Experience')).toBeInTheDocument();
|
||||||
expect(getAllByText('--')).toHaveLength(3);
|
expect(getAllByText('--')).toHaveLength(3);
|
||||||
|
@ -82,17 +104,7 @@ describe('UXSection', () => {
|
||||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||||
refetch: jest.fn(),
|
refetch: jest.fn(),
|
||||||
});
|
});
|
||||||
const { getByText, queryAllByText, getAllByText } = render(
|
const { getByText, queryAllByText, getAllByText } = render(<UXSection bucketSize="60s" />);
|
||||||
<UXSection
|
|
||||||
absoluteTime={{
|
|
||||||
start: moment('2020-06-29T11:38:23.747Z').valueOf(),
|
|
||||||
end: moment('2020-06-29T12:08:23.748Z').valueOf(),
|
|
||||||
}}
|
|
||||||
relativeTime={{ start: 'now-15m', end: 'now' }}
|
|
||||||
bucketSize="60s"
|
|
||||||
serviceName="elastic-co-frontend"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText('User Experience')).toBeInTheDocument();
|
expect(getByText('User Experience')).toBeInTheDocument();
|
||||||
expect(getAllByText('No data is available.')).toHaveLength(3);
|
expect(getAllByText('No data is available.')).toHaveLength(3);
|
||||||
|
|
|
@ -9,28 +9,40 @@ import React from 'react';
|
||||||
import { SectionContainer } from '../';
|
import { SectionContainer } from '../';
|
||||||
import { getDataHandler } from '../../../../data_handler';
|
import { getDataHandler } from '../../../../data_handler';
|
||||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||||
|
import { useHasData } from '../../../../hooks/use_has_data';
|
||||||
|
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||||
|
import { UXHasDataResponse } from '../../../../typings';
|
||||||
import { CoreVitals } from '../../../shared/core_web_vitals';
|
import { CoreVitals } from '../../../shared/core_web_vitals';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serviceName: string;
|
|
||||||
bucketSize: string;
|
bucketSize: string;
|
||||||
absoluteTime: { start?: number; end?: number };
|
|
||||||
relativeTime: { start: string; end: string };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UXSection({ serviceName, bucketSize, absoluteTime, relativeTime }: Props) {
|
export function UXSection({ bucketSize }: Props) {
|
||||||
const { start, end } = absoluteTime;
|
const { forceUpdate, hasData } = useHasData();
|
||||||
|
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
|
||||||
|
const uxHasDataResponse = (hasData.ux?.hasData as UXHasDataResponse) || {};
|
||||||
|
const serviceName = uxHasDataResponse.serviceName as string;
|
||||||
|
|
||||||
const { data, status } = useFetcher(() => {
|
const { data, status } = useFetcher(
|
||||||
if (start && end) {
|
() => {
|
||||||
return getDataHandler('ux')?.fetchData({
|
if (serviceName && bucketSize) {
|
||||||
absoluteTime: { start, end },
|
return getDataHandler('ux')?.fetchData({
|
||||||
relativeTime,
|
absoluteTime: { start: absoluteStart, end: absoluteEnd },
|
||||||
serviceName,
|
relativeTime: { start: relativeStart, end: relativeEnd },
|
||||||
bucketSize,
|
serviceName,
|
||||||
});
|
bucketSize,
|
||||||
}
|
});
|
||||||
}, [start, end, relativeTime, serviceName, bucketSize]);
|
}
|
||||||
|
},
|
||||||
|
// Absolute times shouldn't be used here, since it would refetch on every render
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[bucketSize, relativeStart, relativeEnd, forceUpdate, serviceName]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!uxHasDataResponse?.hasData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const isLoading = status === FETCH_STATUS.LOADING;
|
const isLoading = status === FETCH_STATUS.LOADING;
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { EuiSuperDatePicker } from '@elastic/eui';
|
import { EuiSuperDatePicker } from '@elastic/eui';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
|
import { useHasData } from '../../../hooks/use_has_data';
|
||||||
import { UI_SETTINGS, useKibanaUISettings } from '../../../hooks/use_kibana_ui_settings';
|
import { UI_SETTINGS, useKibanaUISettings } from '../../../hooks/use_kibana_ui_settings';
|
||||||
import { usePluginContext } from '../../../hooks/use_plugin_context';
|
import { usePluginContext } from '../../../hooks/use_plugin_context';
|
||||||
import { fromQuery, toQuery } from '../../../utils/url';
|
import { fromQuery, toQuery } from '../../../utils/url';
|
||||||
|
@ -36,6 +37,7 @@ export function DatePicker({ rangeFrom, rangeTo, refreshPaused, refreshInterval
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { plugins } = usePluginContext();
|
const { plugins } = usePluginContext();
|
||||||
|
const { onRefreshTimeRange } = useHasData();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
plugins.data.query.timefilter.timefilter.setTime({
|
plugins.data.query.timefilter.timefilter.setTime({
|
||||||
|
@ -81,6 +83,7 @@ export function DatePicker({ rangeFrom, rangeTo, refreshPaused, refreshInterval
|
||||||
|
|
||||||
function onTimeChange({ start, end }: { start: string; end: string }) {
|
function onTimeChange({ start, end }: { start: string; end: string }) {
|
||||||
updateUrl({ rangeFrom: start, rangeTo: end });
|
updateUrl({ rangeFrom: start, rangeTo: end });
|
||||||
|
onRefreshTimeRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -0,0 +1,467 @@
|
||||||
|
/*
|
||||||
|
* 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 { act, getByText } from '@testing-library/react';
|
||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { CoreStart } from 'kibana/public';
|
||||||
|
import React from 'react';
|
||||||
|
import { registerDataHandler, unregisterDataHandler } from '../data_handler';
|
||||||
|
import { useHasData } from '../hooks/use_has_data';
|
||||||
|
import * as routeParams from '../hooks/use_route_params';
|
||||||
|
import * as timeRange from '../hooks/use_time_range';
|
||||||
|
import { HasData, ObservabilityFetchDataPlugins } from '../typings/fetch_overview_data';
|
||||||
|
import { HasDataContextProvider } from './has_data_context';
|
||||||
|
import * as pluginContext from '../hooks/use_plugin_context';
|
||||||
|
import { PluginContextValue } from './plugin_context';
|
||||||
|
|
||||||
|
const relativeStart = '2020-10-08T06:00:00.000Z';
|
||||||
|
const relativeEnd = '2020-10-08T07:00:00.000Z';
|
||||||
|
|
||||||
|
function wrapper({ children }: { children: React.ReactElement }) {
|
||||||
|
return <HasDataContextProvider>{children}</HasDataContextProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterAll() {
|
||||||
|
unregisterDataHandler({ appName: 'apm' });
|
||||||
|
unregisterDataHandler({ appName: 'infra_logs' });
|
||||||
|
unregisterDataHandler({ appName: 'infra_metrics' });
|
||||||
|
unregisterDataHandler({ appName: 'uptime' });
|
||||||
|
unregisterDataHandler({ appName: 'ux' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerApps<T extends ObservabilityFetchDataPlugins>(
|
||||||
|
apps: Array<{ appName: T; hasData: HasData<T> }>
|
||||||
|
) {
|
||||||
|
apps.forEach(({ appName, hasData }) => {
|
||||||
|
registerDataHandler({
|
||||||
|
appName,
|
||||||
|
fetchData: () => ({} as any),
|
||||||
|
hasData,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('HasDataContextProvider', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(routeParams, 'useRouteParams').mockImplementation(() => ({
|
||||||
|
query: {
|
||||||
|
from: relativeStart,
|
||||||
|
to: relativeEnd,
|
||||||
|
},
|
||||||
|
path: {},
|
||||||
|
}));
|
||||||
|
jest.spyOn(timeRange, 'useTimeRange').mockImplementation(() => ({
|
||||||
|
relativeStart,
|
||||||
|
relativeEnd,
|
||||||
|
absoluteStart: new Date(relativeStart).valueOf(),
|
||||||
|
absoluteEnd: new Date(relativeEnd).valueOf(),
|
||||||
|
}));
|
||||||
|
jest.spyOn(pluginContext, 'usePluginContext').mockReturnValue({
|
||||||
|
core: ({ http: { get: jest.fn() } } as unknown) as CoreStart,
|
||||||
|
} as PluginContextValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when no plugin has registered', () => {
|
||||||
|
it('hasAnyData returns false and all apps return undefined', async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||||
|
expect(result.current).toMatchObject({
|
||||||
|
hasData: {},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: false,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {
|
||||||
|
apm: { hasData: undefined, status: 'success' },
|
||||||
|
uptime: { hasData: undefined, status: 'success' },
|
||||||
|
infra_logs: { hasData: undefined, status: 'success' },
|
||||||
|
infra_metrics: { hasData: undefined, status: 'success' },
|
||||||
|
ux: { hasData: undefined, status: 'success' },
|
||||||
|
alert: { hasData: [], status: 'success' },
|
||||||
|
},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: true,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when plugins have registered', () => {
|
||||||
|
describe('all apps return false', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
registerApps([
|
||||||
|
{ appName: 'apm', hasData: async () => false },
|
||||||
|
{ appName: 'infra_logs', hasData: async () => false },
|
||||||
|
{ appName: 'infra_metrics', hasData: async () => false },
|
||||||
|
{ appName: 'uptime', hasData: async () => false },
|
||||||
|
{ appName: 'ux', hasData: async () => ({ hasData: false, serviceName: undefined }) },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(unregisterAll);
|
||||||
|
|
||||||
|
it('hasAnyData returns false and all apps return false', async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: false,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {
|
||||||
|
apm: { hasData: false, status: 'success' },
|
||||||
|
uptime: { hasData: false, status: 'success' },
|
||||||
|
infra_logs: { hasData: false, status: 'success' },
|
||||||
|
infra_metrics: { hasData: false, status: 'success' },
|
||||||
|
ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' },
|
||||||
|
alert: { hasData: [], status: 'success' },
|
||||||
|
},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: true,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('at least one app returns true', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
registerApps([
|
||||||
|
{ appName: 'apm', hasData: async () => true },
|
||||||
|
{ appName: 'infra_logs', hasData: async () => false },
|
||||||
|
{ appName: 'infra_metrics', hasData: async () => false },
|
||||||
|
{ appName: 'uptime', hasData: async () => false },
|
||||||
|
{ appName: 'ux', hasData: async () => ({ hasData: false, serviceName: undefined }) },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(unregisterAll);
|
||||||
|
|
||||||
|
it('hasAnyData returns true apm returns true and all other apps return false', async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: false,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {
|
||||||
|
apm: { hasData: true, status: 'success' },
|
||||||
|
uptime: { hasData: false, status: 'success' },
|
||||||
|
infra_logs: { hasData: false, status: 'success' },
|
||||||
|
infra_metrics: { hasData: false, status: 'success' },
|
||||||
|
ux: { hasData: { hasData: false, serviceName: undefined }, status: 'success' },
|
||||||
|
alert: { hasData: [], status: 'success' },
|
||||||
|
},
|
||||||
|
hasAnyData: true,
|
||||||
|
isAllRequestsComplete: true,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('all apps return true', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
registerApps([
|
||||||
|
{ appName: 'apm', hasData: async () => true },
|
||||||
|
{ appName: 'infra_logs', hasData: async () => true },
|
||||||
|
{ appName: 'infra_metrics', hasData: async () => true },
|
||||||
|
{ appName: 'uptime', hasData: async () => true },
|
||||||
|
{ appName: 'ux', hasData: async () => ({ hasData: true, serviceName: 'ux' }) },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(unregisterAll);
|
||||||
|
|
||||||
|
it('hasAnyData returns true and all apps return true', async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: false,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {
|
||||||
|
apm: { hasData: true, status: 'success' },
|
||||||
|
uptime: { hasData: true, status: 'success' },
|
||||||
|
infra_logs: { hasData: true, status: 'success' },
|
||||||
|
infra_metrics: { hasData: true, status: 'success' },
|
||||||
|
ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' },
|
||||||
|
alert: { hasData: [], status: 'success' },
|
||||||
|
},
|
||||||
|
hasAnyData: true,
|
||||||
|
isAllRequestsComplete: true,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('only apm is registered', () => {
|
||||||
|
describe('when apm returns true', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
registerApps([{ appName: 'apm', hasData: async () => true }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(unregisterAll);
|
||||||
|
|
||||||
|
it('hasAnyData returns true, apm returns true and all other apps return undefined', async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useHasData(), {
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: false,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {
|
||||||
|
apm: { hasData: true, status: 'success' },
|
||||||
|
uptime: { hasData: undefined, status: 'success' },
|
||||||
|
infra_logs: { hasData: undefined, status: 'success' },
|
||||||
|
infra_metrics: { hasData: undefined, status: 'success' },
|
||||||
|
ux: { hasData: undefined, status: 'success' },
|
||||||
|
alert: { hasData: [], status: 'success' },
|
||||||
|
},
|
||||||
|
hasAnyData: true,
|
||||||
|
isAllRequestsComplete: true,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when apm returns false', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
registerApps([{ appName: 'apm', hasData: async () => false }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(unregisterAll);
|
||||||
|
|
||||||
|
it('hasAnyData returns false, apm returns false and all other apps return undefined', async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useHasData(), {
|
||||||
|
wrapper,
|
||||||
|
});
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: false,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {
|
||||||
|
apm: { hasData: false, status: 'success' },
|
||||||
|
uptime: { hasData: undefined, status: 'success' },
|
||||||
|
infra_logs: { hasData: undefined, status: 'success' },
|
||||||
|
infra_metrics: { hasData: undefined, status: 'success' },
|
||||||
|
ux: { hasData: undefined, status: 'success' },
|
||||||
|
alert: { hasData: [], status: 'success' },
|
||||||
|
},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: true,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an app throws an error while fetching', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
registerApps([
|
||||||
|
{
|
||||||
|
appName: 'apm',
|
||||||
|
hasData: async () => {
|
||||||
|
throw new Error('BOOMMMMM');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ appName: 'infra_logs', hasData: async () => true },
|
||||||
|
{ appName: 'infra_metrics', hasData: async () => true },
|
||||||
|
{ appName: 'uptime', hasData: async () => true },
|
||||||
|
{ appName: 'ux', hasData: async () => ({ hasData: true, serviceName: 'ux' }) },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(unregisterAll);
|
||||||
|
|
||||||
|
it('hasAnyData returns true, apm is undefined and all other apps return true', async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: false,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {
|
||||||
|
apm: { hasData: undefined, status: 'failure' },
|
||||||
|
uptime: { hasData: true, status: 'success' },
|
||||||
|
infra_logs: { hasData: true, status: 'success' },
|
||||||
|
infra_metrics: { hasData: true, status: 'success' },
|
||||||
|
ux: { hasData: { hasData: true, serviceName: 'ux' }, status: 'success' },
|
||||||
|
alert: { hasData: [], status: 'success' },
|
||||||
|
},
|
||||||
|
hasAnyData: true,
|
||||||
|
isAllRequestsComplete: true,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when all apps throw an error while fetching', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
registerApps([
|
||||||
|
{
|
||||||
|
appName: 'apm',
|
||||||
|
hasData: async () => {
|
||||||
|
throw new Error('BOOMMMMM');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appName: 'infra_logs',
|
||||||
|
hasData: async () => {
|
||||||
|
throw new Error('BOOMMMMM');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appName: 'infra_metrics',
|
||||||
|
hasData: async () => {
|
||||||
|
throw new Error('BOOMMMMM');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appName: 'uptime',
|
||||||
|
hasData: async () => {
|
||||||
|
throw new Error('BOOMMMMM');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appName: 'ux',
|
||||||
|
hasData: async () => {
|
||||||
|
throw new Error('BOOMMMMM');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(unregisterAll);
|
||||||
|
|
||||||
|
it('hasAnyData returns false and all apps return undefined', async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: false,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {
|
||||||
|
apm: { hasData: undefined, status: 'failure' },
|
||||||
|
uptime: { hasData: undefined, status: 'failure' },
|
||||||
|
infra_logs: { hasData: undefined, status: 'failure' },
|
||||||
|
infra_metrics: { hasData: undefined, status: 'failure' },
|
||||||
|
ux: { hasData: undefined, status: 'failure' },
|
||||||
|
alert: { hasData: [], status: 'success' },
|
||||||
|
},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: true,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with alerts', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(pluginContext, 'usePluginContext').mockReturnValue({
|
||||||
|
core: ({
|
||||||
|
http: {
|
||||||
|
get: async () => {
|
||||||
|
return {
|
||||||
|
data: [
|
||||||
|
{ id: 2, consumer: 'apm' },
|
||||||
|
{ id: 3, consumer: 'uptime' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown) as CoreStart,
|
||||||
|
} as PluginContextValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns all alerts available', async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: false,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
hasData: {
|
||||||
|
apm: { hasData: undefined, status: 'success' },
|
||||||
|
uptime: { hasData: undefined, status: 'success' },
|
||||||
|
infra_logs: { hasData: undefined, status: 'success' },
|
||||||
|
infra_metrics: { hasData: undefined, status: 'success' },
|
||||||
|
ux: { hasData: undefined, status: 'success' },
|
||||||
|
alert: {
|
||||||
|
hasData: [
|
||||||
|
{ id: 2, consumer: 'apm' },
|
||||||
|
{ id: 3, consumer: 'uptime' },
|
||||||
|
],
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hasAnyData: false,
|
||||||
|
isAllRequestsComplete: true,
|
||||||
|
forceUpdate: expect.any(String),
|
||||||
|
onRefreshTimeRange: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
125
x-pack/plugins/observability/public/context/has_data_context.tsx
Normal file
125
x-pack/plugins/observability/public/context/has_data_context.tsx
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* 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 { uniqueId } from 'lodash';
|
||||||
|
import React, { createContext, useEffect, useState } from 'react';
|
||||||
|
import { Alert } from '../../../alerts/common';
|
||||||
|
import { getDataHandler } from '../data_handler';
|
||||||
|
import { FETCH_STATUS } from '../hooks/use_fetcher';
|
||||||
|
import { usePluginContext } from '../hooks/use_plugin_context';
|
||||||
|
import { useTimeRange } from '../hooks/use_time_range';
|
||||||
|
import { getObservabilityAlerts } from '../services/get_observability_alerts';
|
||||||
|
import { ObservabilityFetchDataPlugins, UXHasDataResponse } from '../typings/fetch_overview_data';
|
||||||
|
|
||||||
|
type DataContextApps = ObservabilityFetchDataPlugins | 'alert';
|
||||||
|
|
||||||
|
export type HasDataMap = Record<
|
||||||
|
DataContextApps,
|
||||||
|
{ status: FETCH_STATUS; hasData?: boolean | UXHasDataResponse | Alert[] }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface HasDataContextValue {
|
||||||
|
hasData: Partial<HasDataMap>;
|
||||||
|
hasAnyData: boolean;
|
||||||
|
isAllRequestsComplete: boolean;
|
||||||
|
onRefreshTimeRange: () => void;
|
||||||
|
forceUpdate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HasDataContext = createContext({} as HasDataContextValue);
|
||||||
|
|
||||||
|
const apps: DataContextApps[] = ['apm', 'uptime', 'infra_logs', 'infra_metrics', 'ux', 'alert'];
|
||||||
|
|
||||||
|
export function HasDataContextProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const { core } = usePluginContext();
|
||||||
|
const [forceUpdate, setForceUpdate] = useState('');
|
||||||
|
const { absoluteStart, absoluteEnd } = useTimeRange();
|
||||||
|
|
||||||
|
const [hasData, setHasData] = useState<HasDataContextValue['hasData']>({});
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
apps.forEach(async (app) => {
|
||||||
|
try {
|
||||||
|
if (app !== 'alert') {
|
||||||
|
const params =
|
||||||
|
app === 'ux'
|
||||||
|
? { absoluteTime: { start: absoluteStart, end: absoluteEnd } }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const result = await getDataHandler(app)?.hasData(params);
|
||||||
|
setHasData((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[app]: {
|
||||||
|
hasData: result,
|
||||||
|
status: FETCH_STATUS.SUCCESS,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setHasData((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[app]: {
|
||||||
|
hasData: undefined,
|
||||||
|
status: FETCH_STATUS.FAILURE,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchAlerts() {
|
||||||
|
try {
|
||||||
|
const alerts = await getObservabilityAlerts({ core });
|
||||||
|
setHasData((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
alert: {
|
||||||
|
hasData: alerts,
|
||||||
|
status: FETCH_STATUS.SUCCESS,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
setHasData((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
alert: {
|
||||||
|
hasData: undefined,
|
||||||
|
status: FETCH_STATUS.FAILURE,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAlerts();
|
||||||
|
}, [forceUpdate, core]);
|
||||||
|
|
||||||
|
const isAllRequestsComplete = apps.every((app) => {
|
||||||
|
const appStatus = hasData[app]?.status;
|
||||||
|
return appStatus !== undefined && appStatus !== FETCH_STATUS.LOADING;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasAnyData = (Object.keys(hasData) as ObservabilityFetchDataPlugins[]).some(
|
||||||
|
(app) => hasData[app]?.hasData === true
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HasDataContext.Provider
|
||||||
|
value={{
|
||||||
|
hasData,
|
||||||
|
hasAnyData,
|
||||||
|
isAllRequestsComplete,
|
||||||
|
forceUpdate,
|
||||||
|
onRefreshTimeRange: () => {
|
||||||
|
setForceUpdate(uniqueId());
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
children={children}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,20 +3,8 @@
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
import {
|
import { registerDataHandler, getDataHandler } from './data_handler';
|
||||||
registerDataHandler,
|
|
||||||
getDataHandler,
|
|
||||||
unregisterDataHandler,
|
|
||||||
fetchHasData,
|
|
||||||
} from './data_handler';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {
|
|
||||||
ApmFetchDataResponse,
|
|
||||||
LogsFetchDataResponse,
|
|
||||||
MetricsFetchDataResponse,
|
|
||||||
UptimeFetchDataResponse,
|
|
||||||
UxFetchDataResponse,
|
|
||||||
} from './typings';
|
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
absoluteTime: {
|
absoluteTime: {
|
||||||
|
@ -447,203 +435,4 @@ describe('registerDataHandler', () => {
|
||||||
expect(hasData).toBeTruthy();
|
expect(hasData).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('fetchHasData', () => {
|
|
||||||
it('returns false when an exception happens', async () => {
|
|
||||||
unregisterDataHandler({ appName: 'apm' });
|
|
||||||
unregisterDataHandler({ appName: 'infra_logs' });
|
|
||||||
unregisterDataHandler({ appName: 'infra_metrics' });
|
|
||||||
unregisterDataHandler({ appName: 'uptime' });
|
|
||||||
unregisterDataHandler({ appName: 'ux' });
|
|
||||||
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'apm',
|
|
||||||
fetchData: async () => (({} as unknown) as ApmFetchDataResponse),
|
|
||||||
hasData: async () => {
|
|
||||||
throw new Error('BOOM');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'infra_logs',
|
|
||||||
fetchData: async () => (({} as unknown) as LogsFetchDataResponse),
|
|
||||||
hasData: async () => {
|
|
||||||
throw new Error('BOOM');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'infra_metrics',
|
|
||||||
fetchData: async () => (({} as unknown) as MetricsFetchDataResponse),
|
|
||||||
hasData: async () => {
|
|
||||||
throw new Error('BOOM');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'uptime',
|
|
||||||
fetchData: async () => (({} as unknown) as UptimeFetchDataResponse),
|
|
||||||
hasData: async () => {
|
|
||||||
throw new Error('BOOM');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'ux',
|
|
||||||
fetchData: async () => (({} as unknown) as UxFetchDataResponse),
|
|
||||||
hasData: async () => {
|
|
||||||
throw new Error('BOOM');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({
|
|
||||||
apm: false,
|
|
||||||
uptime: false,
|
|
||||||
infra_logs: false,
|
|
||||||
infra_metrics: false,
|
|
||||||
ux: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('returns true when has data and false when an exception happens', async () => {
|
|
||||||
unregisterDataHandler({ appName: 'apm' });
|
|
||||||
unregisterDataHandler({ appName: 'infra_logs' });
|
|
||||||
unregisterDataHandler({ appName: 'infra_metrics' });
|
|
||||||
unregisterDataHandler({ appName: 'uptime' });
|
|
||||||
unregisterDataHandler({ appName: 'ux' });
|
|
||||||
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'apm',
|
|
||||||
fetchData: async () => (({} as unknown) as ApmFetchDataResponse),
|
|
||||||
hasData: async () => true,
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'infra_logs',
|
|
||||||
fetchData: async () => (({} as unknown) as LogsFetchDataResponse),
|
|
||||||
hasData: async () => true,
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'infra_metrics',
|
|
||||||
fetchData: async () => (({} as unknown) as MetricsFetchDataResponse),
|
|
||||||
hasData: async () => {
|
|
||||||
throw new Error('BOOM');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'uptime',
|
|
||||||
fetchData: async () => (({} as unknown) as UptimeFetchDataResponse),
|
|
||||||
hasData: async () => {
|
|
||||||
throw new Error('BOOM');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'ux',
|
|
||||||
fetchData: async () => (({} as unknown) as UxFetchDataResponse),
|
|
||||||
hasData: async () => {
|
|
||||||
throw new Error('BOOM');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({
|
|
||||||
apm: true,
|
|
||||||
uptime: false,
|
|
||||||
infra_logs: true,
|
|
||||||
infra_metrics: false,
|
|
||||||
ux: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('returns true when has data', async () => {
|
|
||||||
unregisterDataHandler({ appName: 'apm' });
|
|
||||||
unregisterDataHandler({ appName: 'infra_logs' });
|
|
||||||
unregisterDataHandler({ appName: 'infra_metrics' });
|
|
||||||
unregisterDataHandler({ appName: 'uptime' });
|
|
||||||
unregisterDataHandler({ appName: 'ux' });
|
|
||||||
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'apm',
|
|
||||||
fetchData: async () => (({} as unknown) as ApmFetchDataResponse),
|
|
||||||
hasData: async () => true,
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'infra_logs',
|
|
||||||
fetchData: async () => (({} as unknown) as LogsFetchDataResponse),
|
|
||||||
hasData: async () => true,
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'infra_metrics',
|
|
||||||
fetchData: async () => (({} as unknown) as MetricsFetchDataResponse),
|
|
||||||
hasData: async () => true,
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'uptime',
|
|
||||||
fetchData: async () => (({} as unknown) as UptimeFetchDataResponse),
|
|
||||||
hasData: async () => true,
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'ux',
|
|
||||||
fetchData: async () => (({} as unknown) as UxFetchDataResponse),
|
|
||||||
hasData: async () => ({
|
|
||||||
hasData: true,
|
|
||||||
serviceName: 'elastic-co',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({
|
|
||||||
apm: true,
|
|
||||||
uptime: true,
|
|
||||||
infra_logs: true,
|
|
||||||
infra_metrics: true,
|
|
||||||
ux: {
|
|
||||||
hasData: true,
|
|
||||||
serviceName: 'elastic-co',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('returns false when has no data', async () => {
|
|
||||||
unregisterDataHandler({ appName: 'apm' });
|
|
||||||
unregisterDataHandler({ appName: 'infra_logs' });
|
|
||||||
unregisterDataHandler({ appName: 'infra_metrics' });
|
|
||||||
unregisterDataHandler({ appName: 'uptime' });
|
|
||||||
unregisterDataHandler({ appName: 'ux' });
|
|
||||||
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'apm',
|
|
||||||
fetchData: async () => (({} as unknown) as ApmFetchDataResponse),
|
|
||||||
hasData: async () => false,
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'infra_logs',
|
|
||||||
fetchData: async () => (({} as unknown) as LogsFetchDataResponse),
|
|
||||||
hasData: async () => false,
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'infra_metrics',
|
|
||||||
fetchData: async () => (({} as unknown) as MetricsFetchDataResponse),
|
|
||||||
hasData: async () => false,
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'uptime',
|
|
||||||
fetchData: async () => (({} as unknown) as UptimeFetchDataResponse),
|
|
||||||
hasData: async () => false,
|
|
||||||
});
|
|
||||||
registerDataHandler({
|
|
||||||
appName: 'ux',
|
|
||||||
fetchData: async () => (({} as unknown) as UxFetchDataResponse),
|
|
||||||
hasData: async () => false,
|
|
||||||
});
|
|
||||||
expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({
|
|
||||||
apm: false,
|
|
||||||
uptime: false,
|
|
||||||
infra_logs: false,
|
|
||||||
infra_metrics: false,
|
|
||||||
ux: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('returns false when has data was not registered', async () => {
|
|
||||||
unregisterDataHandler({ appName: 'apm' });
|
|
||||||
unregisterDataHandler({ appName: 'infra_logs' });
|
|
||||||
unregisterDataHandler({ appName: 'infra_metrics' });
|
|
||||||
unregisterDataHandler({ appName: 'uptime' });
|
|
||||||
unregisterDataHandler({ appName: 'ux' });
|
|
||||||
|
|
||||||
expect(await fetchHasData({ end: 1601632271769, start: 1601631371769 })).toEqual({
|
|
||||||
apm: false,
|
|
||||||
uptime: false,
|
|
||||||
infra_logs: false,
|
|
||||||
infra_metrics: false,
|
|
||||||
ux: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,11 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { DataHandler, ObservabilityFetchDataPlugins } from './typings/fetch_overview_data';
|
||||||
DataHandler,
|
|
||||||
HasDataResponse,
|
|
||||||
ObservabilityFetchDataPlugins,
|
|
||||||
} from './typings/fetch_overview_data';
|
|
||||||
|
|
||||||
const dataHandlers: Partial<Record<ObservabilityFetchDataPlugins, DataHandler>> = {};
|
const dataHandlers: Partial<Record<ObservabilityFetchDataPlugins, DataHandler>> = {};
|
||||||
|
|
||||||
|
@ -34,40 +30,3 @@ export function getDataHandler<T extends ObservabilityFetchDataPlugins>(appName:
|
||||||
return dataHandler as DataHandler<T>;
|
return dataHandler as DataHandler<T>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchHasData(absoluteTime: {
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
}): Promise<Record<ObservabilityFetchDataPlugins, HasDataResponse>> {
|
|
||||||
const apps: ObservabilityFetchDataPlugins[] = [
|
|
||||||
'apm',
|
|
||||||
'uptime',
|
|
||||||
'infra_logs',
|
|
||||||
'infra_metrics',
|
|
||||||
'ux',
|
|
||||||
];
|
|
||||||
|
|
||||||
const promises = apps.map(
|
|
||||||
async (app) =>
|
|
||||||
getDataHandler(app)?.hasData(app === 'ux' ? { absoluteTime } : undefined) || false
|
|
||||||
);
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(promises);
|
|
||||||
|
|
||||||
const [apm, uptime, logs, metrics, ux] = results.map((result) => {
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
return result.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Error while fetching has data', result.reason);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
apm,
|
|
||||||
uptime,
|
|
||||||
ux,
|
|
||||||
infra_logs: logs,
|
|
||||||
infra_metrics: metrics,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
12
x-pack/plugins/observability/public/hooks/use_has_data.ts
Normal file
12
x-pack/plugins/observability/public/hooks/use_has_data.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* 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 { useContext } from 'react';
|
||||||
|
import { HasDataContext } from '../context/has_data_context';
|
||||||
|
|
||||||
|
export function useHasData() {
|
||||||
|
return useContext(HasDataContext);
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import * as t from 'io-ts';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import { isLeft } from 'fp-ts/lib/Either';
|
import { isLeft } from 'fp-ts/lib/Either';
|
||||||
import { PathReporter } from 'io-ts/lib/PathReporter';
|
import { PathReporter } from 'io-ts/lib/PathReporter';
|
||||||
import { Params } from '../routes';
|
import { Params, RouteParams, routes } from '../routes';
|
||||||
|
|
||||||
function getQueryParams(location: ReturnType<typeof useLocation>) {
|
function getQueryParams(location: ReturnType<typeof useLocation>) {
|
||||||
const urlSearchParms = new URLSearchParams(location.search);
|
const urlSearchParms = new URLSearchParams(location.search);
|
||||||
|
@ -23,14 +23,15 @@ function getQueryParams(location: ReturnType<typeof useLocation>) {
|
||||||
* It removes any aditional item which is not declared in the type.
|
* It removes any aditional item which is not declared in the type.
|
||||||
* @param params
|
* @param params
|
||||||
*/
|
*/
|
||||||
export function useRouteParams(params: Params) {
|
export function useRouteParams<T extends keyof typeof routes>(pathName: T): RouteParams<T> {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const pathParams = useParams();
|
const pathParams = useParams();
|
||||||
const queryParams = getQueryParams(location);
|
const queryParams = getQueryParams(location);
|
||||||
|
const { query, path } = routes[pathName].params as Params;
|
||||||
|
|
||||||
const rts = {
|
const rts = {
|
||||||
queryRt: params.query ? t.exact(params.query) : t.strict({}),
|
queryRt: query ? t.exact(query) : t.strict({}),
|
||||||
pathRt: params.path ? t.exact(params.path) : t.strict({}),
|
pathRt: path ? t.exact(path) : t.strict({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryResult = rts.queryRt.decode(queryParams);
|
const queryResult = rts.queryRt.decode(queryParams);
|
||||||
|
@ -43,8 +44,8 @@ export function useRouteParams(params: Params) {
|
||||||
console.error(PathReporter.report(pathResult)[0]);
|
console.error(PathReporter.report(pathResult)[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return ({
|
||||||
query: isLeft(queryResult) ? {} : queryResult.right,
|
query: isLeft(queryResult) ? {} : queryResult.right,
|
||||||
path: isLeft(pathResult) ? {} : pathResult.right,
|
path: isLeft(pathResult) ? {} : pathResult.right,
|
||||||
};
|
} as unknown) as RouteParams<T>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* 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 { useTimeRange } from './use_time_range';
|
||||||
|
import * as pluginContext from './use_plugin_context';
|
||||||
|
import { AppMountParameters, CoreStart } from 'kibana/public';
|
||||||
|
import { ObservabilityPluginSetupDeps } from '../plugin';
|
||||||
|
import * as kibanaUISettings from './use_kibana_ui_settings';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
useLocation: () => ({
|
||||||
|
pathname: '/observability/overview/',
|
||||||
|
search: '',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useTimeRange', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
|
||||||
|
core: {} as CoreStart,
|
||||||
|
appMountParameters: {} as AppMountParameters,
|
||||||
|
plugins: ({
|
||||||
|
data: {
|
||||||
|
query: {
|
||||||
|
timefilter: {
|
||||||
|
timefilter: {
|
||||||
|
getTime: jest.fn().mockImplementation(() => ({
|
||||||
|
from: '2020-10-08T06:00:00.000Z',
|
||||||
|
to: '2020-10-08T07:00:00.000Z',
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown) as ObservabilityPluginSetupDeps,
|
||||||
|
}));
|
||||||
|
jest.spyOn(kibanaUISettings, 'useKibanaUISettings').mockImplementation(() => ({
|
||||||
|
from: '2020-10-08T05:00:00.000Z',
|
||||||
|
to: '2020-10-08T06:00:00.000Z',
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when range from and to are not provided', () => {
|
||||||
|
describe('when data plugin has time set', () => {
|
||||||
|
it('returns ranges and absolute times from data plugin', () => {
|
||||||
|
const relativeStart = '2020-10-08T06:00:00.000Z';
|
||||||
|
const relativeEnd = '2020-10-08T07:00:00.000Z';
|
||||||
|
const timeRange = useTimeRange();
|
||||||
|
expect(timeRange).toEqual({
|
||||||
|
relativeStart,
|
||||||
|
relativeEnd,
|
||||||
|
absoluteStart: new Date(relativeStart).valueOf(),
|
||||||
|
absoluteEnd: new Date(relativeEnd).valueOf(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("when data plugin doesn't have time set", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
|
||||||
|
core: {} as CoreStart,
|
||||||
|
appMountParameters: {} as AppMountParameters,
|
||||||
|
plugins: ({
|
||||||
|
data: {
|
||||||
|
query: {
|
||||||
|
timefilter: {
|
||||||
|
timefilter: {
|
||||||
|
getTime: jest.fn().mockImplementation(() => ({
|
||||||
|
from: undefined,
|
||||||
|
to: undefined,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown) as ObservabilityPluginSetupDeps,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
it('returns ranges and absolute times from kibana default settings', () => {
|
||||||
|
const relativeStart = '2020-10-08T05:00:00.000Z';
|
||||||
|
const relativeEnd = '2020-10-08T06:00:00.000Z';
|
||||||
|
const timeRange = useTimeRange();
|
||||||
|
expect(timeRange).toEqual({
|
||||||
|
relativeStart,
|
||||||
|
relativeEnd,
|
||||||
|
absoluteStart: new Date(relativeStart).valueOf(),
|
||||||
|
absoluteEnd: new Date(relativeEnd).valueOf(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
40
x-pack/plugins/observability/public/hooks/use_time_range.ts
Normal file
40
x-pack/plugins/observability/public/hooks/use_time_range.ts
Normal 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 { parse } from 'query-string';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { TimePickerTime } from '../components/shared/date_picker';
|
||||||
|
import { getAbsoluteTime } from '../utils/date';
|
||||||
|
import { UI_SETTINGS, useKibanaUISettings } from './use_kibana_ui_settings';
|
||||||
|
import { usePluginContext } from './use_plugin_context';
|
||||||
|
|
||||||
|
const getParsedParams = (search: string) => {
|
||||||
|
return parse(search.slice(1), { sort: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useTimeRange() {
|
||||||
|
const { plugins } = usePluginContext();
|
||||||
|
|
||||||
|
const timePickerTimeDefaults = useKibanaUISettings<TimePickerTime>(
|
||||||
|
UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS
|
||||||
|
);
|
||||||
|
|
||||||
|
const timePickerSharedState = plugins.data.query.timefilter.timefilter.getTime();
|
||||||
|
|
||||||
|
const { rangeFrom, rangeTo } = getParsedParams(useLocation().search);
|
||||||
|
|
||||||
|
const relativeStart = (rangeFrom ??
|
||||||
|
timePickerSharedState.from ??
|
||||||
|
timePickerTimeDefaults.from) as string;
|
||||||
|
const relativeEnd = (rangeTo ?? timePickerSharedState.to ?? timePickerTimeDefaults.to) as string;
|
||||||
|
|
||||||
|
return {
|
||||||
|
relativeStart,
|
||||||
|
relativeEnd,
|
||||||
|
absoluteStart: getAbsoluteTime(relativeStart)!,
|
||||||
|
absoluteEnd: getAbsoluteTime(relativeEnd, { roundUp: true })!,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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 { HasDataContextValue } from '../../context/has_data_context';
|
||||||
|
import * as hasData from '../../hooks/use_has_data';
|
||||||
|
import { render } from '../../utils/test_helper';
|
||||||
|
import { HomePage } from './';
|
||||||
|
|
||||||
|
const mockHistoryPush = jest.fn();
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
useHistory: () => ({
|
||||||
|
push: mockHistoryPush,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Home page', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders loading component while requests are not returned', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(hasData, 'useHasData')
|
||||||
|
.mockImplementation(
|
||||||
|
() =>
|
||||||
|
({ hasData: {}, hasAnyData: false, isAllRequestsComplete: false } as HasDataContextValue)
|
||||||
|
);
|
||||||
|
const { getByText } = render(<HomePage />);
|
||||||
|
expect(getByText('Loading Observability')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('renders landing page', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(hasData, 'useHasData')
|
||||||
|
.mockImplementation(
|
||||||
|
() =>
|
||||||
|
({ hasData: {}, hasAnyData: false, isAllRequestsComplete: true } as HasDataContextValue)
|
||||||
|
);
|
||||||
|
render(<HomePage />);
|
||||||
|
expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/landing' });
|
||||||
|
});
|
||||||
|
it('renders overview page', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(hasData, 'useHasData')
|
||||||
|
.mockImplementation(
|
||||||
|
() =>
|
||||||
|
({ hasData: {}, hasAnyData: true, isAllRequestsComplete: false } as HasDataContextValue)
|
||||||
|
);
|
||||||
|
render(<HomePage />);
|
||||||
|
expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/overview' });
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,33 +5,20 @@
|
||||||
*/
|
*/
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { fetchHasData } from '../../data_handler';
|
import { useHasData } from '../../hooks/use_has_data';
|
||||||
import { useFetcher } from '../../hooks/use_fetcher';
|
|
||||||
import { useQueryParams } from '../../hooks/use_query_params';
|
|
||||||
import { LoadingObservability } from '../overview/loading_observability';
|
import { LoadingObservability } from '../overview/loading_observability';
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { hasAnyData, isAllRequestsComplete } = useHasData();
|
||||||
const { absStart, absEnd } = useQueryParams();
|
|
||||||
|
|
||||||
const { data = {} } = useFetcher(
|
|
||||||
() => fetchHasData({ start: absStart, end: absEnd }),
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const values = Object.values(data);
|
|
||||||
const hasSomeData = values.length ? values.some((hasData) => hasData) : null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasSomeData === true) {
|
if (hasAnyData === true) {
|
||||||
history.push({ pathname: '/overview' });
|
history.push({ pathname: '/overview' });
|
||||||
}
|
} else if (hasAnyData === false && isAllRequestsComplete === true) {
|
||||||
if (hasSomeData === false) {
|
|
||||||
history.push({ pathname: '/landing' });
|
history.push({ pathname: '/landing' });
|
||||||
}
|
}
|
||||||
}, [hasSomeData, history]);
|
}, [hasAnyData, isAllRequestsComplete, history]);
|
||||||
|
|
||||||
return <LoadingObservability />;
|
return <LoadingObservability />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,76 +4,39 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||||
|
import React from 'react';
|
||||||
|
import { APMSection } from '../../components/app/section/apm';
|
||||||
import { LogsSection } from '../../components/app/section/logs';
|
import { LogsSection } from '../../components/app/section/logs';
|
||||||
import { MetricsSection } from '../../components/app/section/metrics';
|
import { MetricsSection } from '../../components/app/section/metrics';
|
||||||
import { APMSection } from '../../components/app/section/apm';
|
|
||||||
import { UptimeSection } from '../../components/app/section/uptime';
|
import { UptimeSection } from '../../components/app/section/uptime';
|
||||||
import { UXSection } from '../../components/app/section/ux';
|
import { UXSection } from '../../components/app/section/ux';
|
||||||
import {
|
import { HasDataMap } from '../../context/has_data_context';
|
||||||
HasDataResponse,
|
|
||||||
ObservabilityFetchDataPlugins,
|
|
||||||
UXHasDataResponse,
|
|
||||||
} from '../../typings/fetch_overview_data';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bucketSize: string;
|
bucketSize: string;
|
||||||
absoluteTime: { start?: number; end?: number };
|
hasData?: Partial<HasDataMap>;
|
||||||
relativeTime: { start: string; end: string };
|
|
||||||
hasData: Record<ObservabilityFetchDataPlugins, HasDataResponse>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataSections({ bucketSize, hasData, absoluteTime, relativeTime }: Props) {
|
export function DataSections({ bucketSize }: Props) {
|
||||||
return (
|
return (
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiFlexGroup direction="column">
|
<EuiFlexGroup direction="column">
|
||||||
{hasData?.infra_logs && (
|
<EuiFlexItem grow={false}>
|
||||||
<EuiFlexItem grow={false}>
|
<LogsSection bucketSize={bucketSize} />
|
||||||
<LogsSection
|
</EuiFlexItem>
|
||||||
bucketSize={bucketSize}
|
<EuiFlexItem grow={false}>
|
||||||
absoluteTime={absoluteTime}
|
<MetricsSection bucketSize={bucketSize} />
|
||||||
relativeTime={relativeTime}
|
</EuiFlexItem>
|
||||||
/>
|
<EuiFlexItem grow={false}>
|
||||||
</EuiFlexItem>
|
<APMSection bucketSize={bucketSize} />
|
||||||
)}
|
</EuiFlexItem>
|
||||||
{hasData?.infra_metrics && (
|
<EuiFlexItem grow={false}>
|
||||||
<EuiFlexItem grow={false}>
|
<UptimeSection bucketSize={bucketSize} />
|
||||||
<MetricsSection
|
</EuiFlexItem>
|
||||||
bucketSize={bucketSize}
|
<EuiFlexItem grow={false}>
|
||||||
absoluteTime={absoluteTime}
|
<UXSection bucketSize={bucketSize} />
|
||||||
relativeTime={relativeTime}
|
</EuiFlexItem>
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
)}
|
|
||||||
{hasData?.apm && (
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<APMSection
|
|
||||||
bucketSize={bucketSize}
|
|
||||||
absoluteTime={absoluteTime}
|
|
||||||
relativeTime={relativeTime}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
)}
|
|
||||||
{hasData?.uptime && (
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<UptimeSection
|
|
||||||
bucketSize={bucketSize}
|
|
||||||
absoluteTime={absoluteTime}
|
|
||||||
relativeTime={relativeTime}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
)}
|
|
||||||
{(hasData.ux as UXHasDataResponse).hasData && (
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<UXSection
|
|
||||||
serviceName={(hasData.ux as UXHasDataResponse).serviceName as string}
|
|
||||||
bucketSize={bucketSize}
|
|
||||||
absoluteTime={absoluteTime}
|
|
||||||
relativeTime={relativeTime}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
)}
|
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,27 +3,25 @@
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { ThemeContext } from 'styled-components';
|
import { ThemeContext } from 'styled-components';
|
||||||
import { useTrackPageview, UXHasDataResponse } from '../..';
|
import { useTrackPageview } from '../..';
|
||||||
import { EmptySection } from '../../components/app/empty_section';
|
import { Alert } from '../../../../alerts/common';
|
||||||
|
import { EmptySections } from '../../components/app/empty_sections';
|
||||||
import { WithHeaderLayout } from '../../components/app/layout/with_header';
|
import { WithHeaderLayout } from '../../components/app/layout/with_header';
|
||||||
import { NewsFeed } from '../../components/app/news_feed';
|
import { NewsFeed } from '../../components/app/news_feed';
|
||||||
import { Resources } from '../../components/app/resources';
|
import { Resources } from '../../components/app/resources';
|
||||||
import { AlertsSection } from '../../components/app/section/alerts';
|
import { AlertsSection } from '../../components/app/section/alerts';
|
||||||
import { DatePicker, TimePickerTime } from '../../components/shared/date_picker';
|
import { DatePicker } from '../../components/shared/date_picker';
|
||||||
import { fetchHasData } from '../../data_handler';
|
import { useFetcher } from '../../hooks/use_fetcher';
|
||||||
import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher';
|
import { useHasData } from '../../hooks/use_has_data';
|
||||||
import { UI_SETTINGS, useKibanaUISettings } from '../../hooks/use_kibana_ui_settings';
|
|
||||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||||
|
import { useTimeRange } from '../../hooks/use_time_range';
|
||||||
import { RouteParams } from '../../routes';
|
import { RouteParams } from '../../routes';
|
||||||
import { getNewsFeed } from '../../services/get_news_feed';
|
import { getNewsFeed } from '../../services/get_news_feed';
|
||||||
import { getObservabilityAlerts } from '../../services/get_observability_alerts';
|
|
||||||
import { getAbsoluteTime } from '../../utils/date';
|
|
||||||
import { getBucketSize } from '../../utils/get_bucket_size';
|
import { getBucketSize } from '../../utils/get_bucket_size';
|
||||||
import { DataSections } from './data_sections';
|
import { DataSections } from './data_sections';
|
||||||
import { getEmptySections } from './empty_section';
|
|
||||||
import { LoadingObservability } from './loading_observability';
|
import { LoadingObservability } from './loading_observability';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -37,47 +35,26 @@ function calculateBucketSize({ start, end }: { start?: number; end?: number }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OverviewPage({ routeParams }: Props) {
|
export function OverviewPage({ routeParams }: Props) {
|
||||||
const { core, plugins } = usePluginContext();
|
|
||||||
|
|
||||||
// read time from state and update the url
|
|
||||||
const timePickerSharedState = plugins.data.query.timefilter.timefilter.getTime();
|
|
||||||
|
|
||||||
const timePickerDefaults = useKibanaUISettings<TimePickerTime>(
|
|
||||||
UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS
|
|
||||||
);
|
|
||||||
|
|
||||||
const relativeTime = {
|
|
||||||
start: routeParams.query.rangeFrom || timePickerSharedState.from || timePickerDefaults.from,
|
|
||||||
end: routeParams.query.rangeTo || timePickerSharedState.to || timePickerDefaults.to,
|
|
||||||
};
|
|
||||||
|
|
||||||
const absoluteTime = {
|
|
||||||
start: getAbsoluteTime(relativeTime.start) as number,
|
|
||||||
end: getAbsoluteTime(relativeTime.end, { roundUp: true }) as number,
|
|
||||||
};
|
|
||||||
|
|
||||||
useTrackPageview({ app: 'observability-overview', path: 'overview' });
|
useTrackPageview({ app: 'observability-overview', path: 'overview' });
|
||||||
useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 });
|
useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 });
|
||||||
|
const { core } = usePluginContext();
|
||||||
|
const theme = useContext(ThemeContext);
|
||||||
|
|
||||||
const { data: alerts = [], status: alertStatus } = useFetcher(() => {
|
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
|
||||||
return getObservabilityAlerts({ core });
|
|
||||||
}, [core]);
|
const relativeTime = { start: relativeStart, end: relativeEnd };
|
||||||
|
const absoluteTime = { start: absoluteStart, end: absoluteEnd };
|
||||||
|
|
||||||
const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]);
|
const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]);
|
||||||
|
|
||||||
const theme = useContext(ThemeContext);
|
const { hasData, hasAnyData } = useHasData();
|
||||||
|
|
||||||
const result = useFetcher(
|
if (hasAnyData === undefined) {
|
||||||
() => fetchHasData(absoluteTime),
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const hasData = result.data;
|
|
||||||
|
|
||||||
if (!hasData) {
|
|
||||||
return <LoadingObservability />;
|
return <LoadingObservability />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const alerts = (hasData.alert?.hasData as Alert[]) || [];
|
||||||
|
|
||||||
const { refreshInterval = 10000, refreshPaused = true } = routeParams.query;
|
const { refreshInterval = 10000, refreshPaused = true } = routeParams.query;
|
||||||
|
|
||||||
const bucketSize = calculateBucketSize({
|
const bucketSize = calculateBucketSize({
|
||||||
|
@ -85,18 +62,6 @@ export function OverviewPage({ routeParams }: Props) {
|
||||||
end: absoluteTime.end,
|
end: absoluteTime.end,
|
||||||
});
|
});
|
||||||
|
|
||||||
const appEmptySections = getEmptySections({ core }).filter(({ id }) => {
|
|
||||||
if (id === 'alert') {
|
|
||||||
return alertStatus !== FETCH_STATUS.FAILURE && !alerts.length;
|
|
||||||
} else if (id === 'ux') {
|
|
||||||
return !(hasData[id] as UXHasDataResponse).hasData;
|
|
||||||
}
|
|
||||||
return !hasData[id];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hides the data section when all 'hasData' is false or undefined
|
|
||||||
const showDataSections = Object.values(hasData).some((hasPluginData) => hasPluginData);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WithHeaderLayout
|
<WithHeaderLayout
|
||||||
headerColor={theme.eui.euiColorEmptyShade}
|
headerColor={theme.eui.euiColorEmptyShade}
|
||||||
|
@ -113,42 +78,9 @@ export function OverviewPage({ routeParams }: Props) {
|
||||||
<EuiFlexGroup>
|
<EuiFlexGroup>
|
||||||
<EuiFlexItem grow={6}>
|
<EuiFlexItem grow={6}>
|
||||||
{/* Data sections */}
|
{/* Data sections */}
|
||||||
{showDataSections && (
|
{hasAnyData && <DataSections bucketSize={bucketSize?.intervalString!} />}
|
||||||
<DataSections
|
|
||||||
hasData={hasData}
|
|
||||||
absoluteTime={absoluteTime}
|
|
||||||
relativeTime={relativeTime}
|
|
||||||
bucketSize={bucketSize?.intervalString!}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Empty sections */}
|
<EmptySections />
|
||||||
{!!appEmptySections.length && (
|
|
||||||
<EuiFlexItem>
|
|
||||||
<EuiSpacer size="s" />
|
|
||||||
<EuiFlexGrid
|
|
||||||
columns={
|
|
||||||
// when more than 2 empty sections are available show them on 2 columns, otherwise 1
|
|
||||||
appEmptySections.length > 2 ? 2 : 1
|
|
||||||
}
|
|
||||||
gutterSize="s"
|
|
||||||
>
|
|
||||||
{appEmptySections.map((app) => {
|
|
||||||
return (
|
|
||||||
<EuiFlexItem
|
|
||||||
key={app.id}
|
|
||||||
style={{
|
|
||||||
border: `1px dashed ${theme.eui.euiBorderColor}`,
|
|
||||||
borderRadius: '4px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EmptySection section={app} />
|
|
||||||
</EuiFlexItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</EuiFlexGrid>
|
|
||||||
</EuiFlexItem>
|
|
||||||
)}
|
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
||||||
{/* Alert section */}
|
{/* Alert section */}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { AppMountParameters, CoreStart } from 'kibana/public';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { UI_SETTINGS } from '../../../../../../src/plugins/data/public';
|
import { UI_SETTINGS } from '../../../../../../src/plugins/data/public';
|
||||||
|
import { HasDataContextProvider } from '../../context/has_data_context';
|
||||||
import { PluginContext } from '../../context/plugin_context';
|
import { PluginContext } from '../../context/plugin_context';
|
||||||
import { registerDataHandler, unregisterDataHandler } from '../../data_handler';
|
import { registerDataHandler, unregisterDataHandler } from '../../data_handler';
|
||||||
import { ObservabilityPluginSetupDeps } from '../../plugin';
|
import { ObservabilityPluginSetupDeps } from '../../plugin';
|
||||||
|
@ -52,7 +53,9 @@ const withCore = makeDecorator({
|
||||||
} as unknown) as ObservabilityPluginSetupDeps,
|
} as unknown) as ObservabilityPluginSetupDeps,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EuiThemeProvider>{storyFn(context)}</EuiThemeProvider>
|
<EuiThemeProvider>
|
||||||
|
<HasDataContextProvider>{storyFn(context)}</HasDataContextProvider>
|
||||||
|
</EuiThemeProvider>
|
||||||
</PluginContext.Provider>
|
</PluginContext.Provider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AppMountContext } from 'kibana/public';
|
import { CoreStart } from 'kibana/public';
|
||||||
import { getObservabilityAlerts } from './get_observability_alerts';
|
import { getObservabilityAlerts } from './get_observability_alerts';
|
||||||
|
|
||||||
const basePath = { prepend: (path: string) => path };
|
const basePath = { prepend: (path: string) => path };
|
||||||
|
@ -27,10 +27,9 @@ describe('getObservabilityAlerts', () => {
|
||||||
},
|
},
|
||||||
basePath,
|
basePath,
|
||||||
},
|
},
|
||||||
} as unknown) as AppMountContext['core'];
|
} as unknown) as CoreStart;
|
||||||
|
|
||||||
const alerts = await getObservabilityAlerts({ core });
|
expect(getObservabilityAlerts({ core })).rejects.toThrow('Boom');
|
||||||
expect(alerts).toEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns empty array when api return undefined', async () => {
|
it('Returns empty array when api return undefined', async () => {
|
||||||
|
@ -43,7 +42,7 @@ describe('getObservabilityAlerts', () => {
|
||||||
},
|
},
|
||||||
basePath,
|
basePath,
|
||||||
},
|
},
|
||||||
} as unknown) as AppMountContext['core'];
|
} as unknown) as CoreStart;
|
||||||
|
|
||||||
const alerts = await getObservabilityAlerts({ core });
|
const alerts = await getObservabilityAlerts({ core });
|
||||||
expect(alerts).toEqual([]);
|
expect(alerts).toEqual([]);
|
||||||
|
@ -55,32 +54,17 @@ describe('getObservabilityAlerts', () => {
|
||||||
get: async () => {
|
get: async () => {
|
||||||
return {
|
return {
|
||||||
data: [
|
data: [
|
||||||
{
|
{ id: 1, consumer: 'siem' },
|
||||||
id: 1,
|
{ id: 2, consumer: 'kibana' },
|
||||||
consumer: 'siem',
|
{ id: 3, consumer: 'index' },
|
||||||
},
|
{ id: 4, consumer: 'foo' },
|
||||||
{
|
{ id: 5, consumer: 'bar' },
|
||||||
id: 2,
|
|
||||||
consumer: 'kibana',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
consumer: 'index',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
consumer: 'foo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
consumer: 'bar',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
basePath,
|
basePath,
|
||||||
},
|
},
|
||||||
} as unknown) as AppMountContext['core'];
|
} as unknown) as CoreStart;
|
||||||
const alerts = await getObservabilityAlerts({ core });
|
const alerts = await getObservabilityAlerts({ core });
|
||||||
expect(alerts).toEqual([]);
|
expect(alerts).toEqual([]);
|
||||||
});
|
});
|
||||||
|
@ -91,36 +75,18 @@ describe('getObservabilityAlerts', () => {
|
||||||
get: async () => {
|
get: async () => {
|
||||||
return {
|
return {
|
||||||
data: [
|
data: [
|
||||||
{
|
{ id: 1, consumer: 'siem' },
|
||||||
id: 1,
|
{ id: 2, consumer: 'apm' },
|
||||||
consumer: 'siem',
|
{ id: 3, consumer: 'uptime' },
|
||||||
},
|
{ id: 4, consumer: 'logs' },
|
||||||
{
|
{ id: 5, consumer: 'metrics' },
|
||||||
id: 2,
|
{ id: 6, consumer: 'alerts' },
|
||||||
consumer: 'apm',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
consumer: 'uptime',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
consumer: 'logs',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
consumer: 'metrics',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
consumer: 'alerts',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
basePath,
|
basePath,
|
||||||
},
|
},
|
||||||
} as unknown) as AppMountContext['core'];
|
} as unknown) as CoreStart;
|
||||||
|
|
||||||
const alerts = await getObservabilityAlerts({ core });
|
const alerts = await getObservabilityAlerts({ core });
|
||||||
expect(alerts).toEqual([
|
expect(alerts).toEqual([
|
||||||
|
|
|
@ -4,23 +4,24 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AppMountContext } from 'kibana/public';
|
import { CoreStart } from 'kibana/public';
|
||||||
import { Alert } from '../../../alerts/common';
|
import { Alert } from '../../../alerts/common';
|
||||||
|
|
||||||
const allowedConsumers = ['apm', 'uptime', 'logs', 'metrics', 'alerts'];
|
const allowedConsumers = ['apm', 'uptime', 'logs', 'metrics', 'alerts'];
|
||||||
|
|
||||||
export async function getObservabilityAlerts({ core }: { core: AppMountContext['core'] }) {
|
export async function getObservabilityAlerts({ core }: { core: CoreStart }) {
|
||||||
try {
|
try {
|
||||||
const { data = [] }: { data: Alert[] } = await core.http.get('/api/alerts/_find', {
|
const { data = [] }: { data: Alert[] } =
|
||||||
query: {
|
(await core.http.get('/api/alerts/_find', {
|
||||||
page: 1,
|
query: {
|
||||||
per_page: 20,
|
page: 1,
|
||||||
},
|
per_page: 20,
|
||||||
});
|
},
|
||||||
|
})) || {};
|
||||||
|
|
||||||
return data.filter(({ consumer }) => allowedConsumers.includes(consumer));
|
return data.filter(({ consumer }) => allowedConsumers.includes(consumer));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error while fetching alerts', e);
|
console.error('Error while fetching alerts', e);
|
||||||
return [];
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,13 +37,13 @@ export interface UXHasDataResponse {
|
||||||
serviceName: string | number | undefined;
|
serviceName: string | number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HasDataResponse = UXHasDataResponse | boolean;
|
|
||||||
|
|
||||||
export type FetchData<T extends FetchDataResponse = FetchDataResponse> = (
|
export type FetchData<T extends FetchDataResponse = FetchDataResponse> = (
|
||||||
fetchDataParams: FetchDataParams
|
fetchDataParams: FetchDataParams
|
||||||
) => Promise<T>;
|
) => Promise<T>;
|
||||||
|
|
||||||
export type HasData = (params?: HasDataParams) => Promise<HasDataResponse>;
|
export type HasData<T extends ObservabilityFetchDataPlugins> = (
|
||||||
|
params?: HasDataParams
|
||||||
|
) => Promise<ObservabilityHasDataResponse[T]>;
|
||||||
|
|
||||||
export type ObservabilityFetchDataPlugins = Exclude<
|
export type ObservabilityFetchDataPlugins = Exclude<
|
||||||
ObservabilityApp,
|
ObservabilityApp,
|
||||||
|
@ -54,7 +54,7 @@ export interface DataHandler<
|
||||||
T extends ObservabilityFetchDataPlugins = ObservabilityFetchDataPlugins
|
T extends ObservabilityFetchDataPlugins = ObservabilityFetchDataPlugins
|
||||||
> {
|
> {
|
||||||
fetchData: FetchData<ObservabilityFetchDataResponse[T]>;
|
fetchData: FetchData<ObservabilityFetchDataResponse[T]>;
|
||||||
hasData: HasData;
|
hasData: HasData<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FetchDataResponse {
|
export interface FetchDataResponse {
|
||||||
|
@ -113,3 +113,11 @@ export interface ObservabilityFetchDataResponse {
|
||||||
uptime: UptimeFetchDataResponse;
|
uptime: UptimeFetchDataResponse;
|
||||||
ux: UxFetchDataResponse;
|
ux: UxFetchDataResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ObservabilityHasDataResponse {
|
||||||
|
apm: boolean;
|
||||||
|
infra_metrics: boolean;
|
||||||
|
infra_logs: boolean;
|
||||||
|
uptime: boolean;
|
||||||
|
ux: UXHasDataResponse;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue