[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:
Cauê Marcondes 2020-11-23 11:58:49 +01:00 committed by GitHub
parent e3ca8a928d
commit ac73b6a5b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1212 additions and 638 deletions

View file

@ -8,6 +8,7 @@ import {
FetchDataParams,
HasDataParams,
UxFetchDataResponse,
UXHasDataResponse,
} from '../../../../../observability/public/';
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({
endpoint: 'GET /api/apm/observability_overview/has_rum_data',
params: {

View file

@ -54,6 +54,6 @@ export async function hasRumData({ setup }: { setup: Setup & SetupTimeRange }) {
response.aggregations?.services?.mostTraffic?.buckets?.[0]?.key,
};
} catch (e) {
return false;
return { hasData: false, serviceName: undefined };
}
}

View file

@ -6,12 +6,7 @@
import { encode } from 'rison-node';
import { SearchResponse } from 'elasticsearch';
import {
FetchData,
FetchDataParams,
HasData,
LogsFetchDataResponse,
} from '../../../observability/public';
import { FetchData, FetchDataParams, LogsFetchDataResponse } from '../../../observability/public';
import { DEFAULT_SOURCE_ID } from '../../common/constants';
import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration';
import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status';
@ -38,9 +33,7 @@ interface LogParams {
type StatsAndSeries = Pick<LogsFetchDataResponse, 'stats' | 'series'>;
export function getLogsHasDataFetcher(
getStartServices: InfraClientCoreSetup['getStartServices']
): HasData {
export function getLogsHasDataFetcher(getStartServices: InfraClientCoreSetup['getStartServices']) {
return async () => {
const [core] = await getStartServices();
const sourceStatus = await callFetchLogSourceStatusAPI(DEFAULT_SOURCE_ID, core.http.fetch);

View file

@ -11,9 +11,25 @@ import { ObservabilityPluginSetupDeps } from '../plugin';
import { renderApp } from './';
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 () => {
const plugins = ({
usageCollection: { reportUiStats: () => {} },
data: {
query: {
timefilter: {
timefilter: { setTime: jest.fn(), getTime: jest.fn().mockImplementation(() => ({})) },
},
},
},
} as unknown) as ObservabilityPluginSetupDeps;
const core = ({
application: { currentAppId$: new Observable(), navigateToUrl: () => {} },

View file

@ -17,6 +17,7 @@ import { PluginContext } from '../context/plugin_context';
import { usePluginContext } from '../hooks/use_plugin_context';
import { useRouteParams } from '../hooks/use_route_params';
import { ObservabilityPluginSetupDeps } from '../plugin';
import { HasDataContextProvider } from '../context/has_data_context';
import { Breadcrumbs, routes } from '../routes';
const observabilityLabelBreadcrumb = {
@ -46,8 +47,8 @@ function App() {
core.chrome.docTitle.change(getTitleFromBreadCrumbs(breadcrumb));
}, [core, breadcrumb]);
const { query, path: pathParams } = useRouteParams(route.params);
return route.handler({ query, path: pathParams });
const params = useRouteParams(path);
return route.handler(params);
};
return <Route key={path} path={path} exact={true} component={Wrapper} />;
})}
@ -79,7 +80,9 @@ export const renderApp = (
<EuiThemeProvider darkMode={isDarkMode}>
<i18nCore.Context>
<RedirectAppLinks application={core.application}>
<App />
<HasDataContextProvider>
<App />
</HasDataContextProvider>
</RedirectAppLinks>
</i18nCore.Context>
</EuiThemeProvider>

View file

@ -6,7 +6,7 @@
import React from 'react';
import { ISection } from '../../../typings/section';
import { render } from '../../../utils/test_helper';
import { EmptySection } from './';
import { EmptySection } from './empty_section';
describe('EmptySection', () => {
it('renders without action button', () => {

View file

@ -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>
);
}

View file

@ -8,25 +8,59 @@ import * as fetcherHook from '../../../../hooks/use_fetcher';
import { render } from '../../../../utils/test_helper';
import { APMSection } from './';
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', () => {
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', () => {
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
data: response,
status: fetcherHook.FETCH_STATUS.SUCCESS,
refetch: jest.fn(),
});
const { getByText, queryAllByTestId } = render(
<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"
/>
);
const { getByText, queryAllByTestId } = render(<APMSection bucketSize="60s" />);
expect(getByText('APM')).toBeInTheDocument();
expect(getByText('View in app')).toBeInTheDocument();
@ -40,16 +74,7 @@ describe('APMSection', () => {
status: fetcherHook.FETCH_STATUS.LOADING,
refetch: jest.fn(),
});
const { getByText, queryAllByText, getByTestId } = render(
<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"
/>
);
const { getByText, queryAllByText, getByTestId } = render(<APMSection bucketSize="60s" />);
expect(getByText('APM')).toBeInTheDocument();
expect(getByTestId('loading')).toBeInTheDocument();

View file

@ -12,17 +12,17 @@ import moment from 'moment';
import React, { useContext } from 'react';
import { useHistory } from 'react-router-dom';
import { ThemeContext } from 'styled-components';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { SectionContainer } from '../';
import { getDataHandler } from '../../../../data_handler';
import { useChartTheme } from '../../../../hooks/use_chart_theme';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useHasData } from '../../../../hooks/use_has_data';
import { ChartContainer } from '../../chart_container';
import { StyledStat } from '../../styled_stat';
import { onBrushEnd } from '../helper';
interface Props {
absoluteTime: { start?: number; end?: number };
relativeTime: { start: string; end: string };
bucketSize?: string;
}
@ -30,25 +30,36 @@ function formatTpm(value?: number) {
return numeral(value).format('0.00a');
}
export function APMSection({ absoluteTime, relativeTime, bucketSize }: Props) {
export function APMSection({ bucketSize }: Props) {
const theme = useContext(ThemeContext);
const chartTheme = useChartTheme();
const history = useHistory();
const { forceUpdate, hasData } = useHasData();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const { start, end } = absoluteTime;
const { data, status } = useFetcher(() => {
if (start && end && bucketSize) {
return getDataHandler('apm')?.fetchData({
absoluteTime: { start, end },
relativeTime,
bucketSize,
});
}
}, [start, end, bucketSize, relativeTime]);
const { data, status } = useFetcher(
() => {
if (bucketSize) {
return getDataHandler('apm')?.fetchData({
absoluteTime: { start: absoluteStart, end: absoluteEnd },
relativeTime: { start: relativeStart, end: relativeEnd },
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]
);
if (!hasData.apm?.hasData) {
return null;
}
const { appLink, stats, series } = data || {};
const min = moment.utc(absoluteTime.start).valueOf();
const max = moment.utc(absoluteTime.end).valueOf();
const min = moment.utc(absoluteStart).valueOf();
const max = moment.utc(absoluteEnd).valueOf();
const formatter = niceTimeFormatter([min, max]);
@ -93,7 +104,7 @@ export function APMSection({ absoluteTime, relativeTime, bucketSize }: Props) {
<ChartContainer isInitialLoad={isLoading && !data}>
<Settings
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
theme={useChartTheme()}
theme={chartTheme}
showLegend={false}
xDomain={{ min, max }}
/>

View file

@ -5,19 +5,19 @@
*/
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 { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import moment from 'moment';
import React, { Fragment } from 'react';
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 { getDataHandler } from '../../../../data_handler';
import { useChartTheme } from '../../../../hooks/use_chart_theme';
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 { formatStatValue } from '../../../../utils/format_stat_value';
import { ChartContainer } from '../../chart_container';
@ -25,8 +25,6 @@ import { StyledStat } from '../../styled_stat';
import { onBrushEnd } from '../helper';
interface Props {
absoluteTime: { start?: number; end?: number };
relativeTime: { start: string; end: string };
bucketSize?: string;
}
@ -45,22 +43,33 @@ function getColorPerItem(series?: LogsFetchDataResponse['series']) {
return colorsPerItem;
}
export function LogsSection({ absoluteTime, relativeTime, bucketSize }: Props) {
export function LogsSection({ bucketSize }: Props) {
const history = useHistory();
const chartTheme = useChartTheme();
const { forceUpdate, hasData } = useHasData();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const { start, end } = absoluteTime;
const { data, status } = useFetcher(() => {
if (start && end && bucketSize) {
return getDataHandler('infra_logs')?.fetchData({
absoluteTime: { start, end },
relativeTime,
bucketSize,
});
}
}, [start, end, bucketSize, relativeTime]);
const { data, status } = useFetcher(
() => {
if (bucketSize) {
return getDataHandler('infra_logs')?.fetchData({
absoluteTime: { start: absoluteStart, end: absoluteEnd },
relativeTime: { start: relativeStart, end: relativeEnd },
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]
);
const min = moment.utc(absoluteTime.start).valueOf();
const max = moment.utc(absoluteTime.end).valueOf();
if (!hasData.infra_logs?.hasData) {
return null;
}
const min = moment.utc(absoluteStart).valueOf();
const max = moment.utc(absoluteEnd).valueOf();
const formatter = niceTimeFormatter([min, max]);
@ -115,7 +124,7 @@ export function LogsSection({ absoluteTime, relativeTime, bucketSize }: Props) {
<ChartContainer isInitialLoad={isLoading && !data}>
<Settings
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
theme={useChartTheme()}
theme={chartTheme}
showLegend
legendPosition={Position.Right}
xDomain={{ min, max }}

View file

@ -13,13 +13,13 @@ import { SectionContainer } from '../';
import { getDataHandler } from '../../../../data_handler';
import { useChartTheme } from '../../../../hooks/use_chart_theme';
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 { ChartContainer } from '../../chart_container';
import { StyledStat } from '../../styled_stat';
interface Props {
absoluteTime: { start?: number; end?: number };
relativeTime: { start: string; end: 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 { forceUpdate, hasData } = useHasData();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const { start, end } = absoluteTime;
const { data, status } = useFetcher(() => {
if (start && end && bucketSize) {
return getDataHandler('infra_metrics')?.fetchData({
absoluteTime: { start, end },
relativeTime,
bucketSize,
});
}
}, [start, end, bucketSize, relativeTime]);
const { data, status } = useFetcher(
() => {
if (bucketSize) {
return getDataHandler('infra_metrics')?.fetchData({
absoluteTime: { start: absoluteStart, end: absoluteEnd },
relativeTime: { start: relativeStart, end: relativeEnd },
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]
);
if (!hasData.infra_metrics?.hasData) {
return null;
}
const isLoading = status === FETCH_STATUS.LOADING;

View file

@ -24,34 +24,45 @@ import { SectionContainer } from '../';
import { getDataHandler } from '../../../../data_handler';
import { useChartTheme } from '../../../../hooks/use_chart_theme';
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 { ChartContainer } from '../../chart_container';
import { StyledStat } from '../../styled_stat';
import { onBrushEnd } from '../helper';
interface Props {
absoluteTime: { start?: number; end?: number };
relativeTime: { start: string; end: string };
bucketSize?: string;
}
export function UptimeSection({ absoluteTime, relativeTime, bucketSize }: Props) {
export function UptimeSection({ bucketSize }: Props) {
const theme = useContext(ThemeContext);
const chartTheme = useChartTheme();
const history = useHistory();
const { forceUpdate, hasData } = useHasData();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const { start, end } = absoluteTime;
const { data, status } = useFetcher(() => {
if (start && end && bucketSize) {
return getDataHandler('uptime')?.fetchData({
absoluteTime: { start, end },
relativeTime,
bucketSize,
});
}
}, [start, end, bucketSize, relativeTime]);
const { data, status } = useFetcher(
() => {
if (bucketSize) {
return getDataHandler('uptime')?.fetchData({
absoluteTime: { start: absoluteStart, end: absoluteEnd },
relativeTime: { start: relativeStart, end: relativeEnd },
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]
);
const min = moment.utc(absoluteTime.start).valueOf();
const max = moment.utc(absoluteTime.end).valueOf();
if (!hasData.uptime?.hasData) {
return null;
}
const min = moment.utc(absoluteStart).valueOf();
const max = moment.utc(absoluteEnd).valueOf();
const formatter = niceTimeFormatter([min, max]);
@ -112,7 +123,7 @@ export function UptimeSection({ absoluteTime, relativeTime, bucketSize }: Props)
<ChartContainer isInitialLoad={isLoading && !data}>
<Settings
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
theme={useChartTheme()}
theme={chartTheme}
showLegend={false}
legendPosition={Position.Right}
xDomain={{ min, max }}

View file

@ -3,31 +3,63 @@
* or more contributor license agreements. Licensed under 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 moment from 'moment';
import { HasDataContextValue } from '../../../../context/has_data_context';
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 { UXSection } from './';
import { response } from './mock_data/ux.mock';
jest.mock('react-router-dom', () => ({
useLocation: () => ({
pathname: '/observability/overview/',
search: '',
}),
}));
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', () => {
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
data: response,
status: fetcherHook.FETCH_STATUS.SUCCESS,
refetch: jest.fn(),
});
const { getByText, getAllByText } = render(
<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"
/>
);
const { getByText, getAllByText } = render(<UXSection bucketSize="60s" />);
expect(getByText('User Experience')).toBeInTheDocument();
expect(getByText('View in app')).toBeInTheDocument();
@ -59,17 +91,7 @@ describe('UXSection', () => {
status: fetcherHook.FETCH_STATUS.LOADING,
refetch: jest.fn(),
});
const { getByText, queryAllByText, getAllByText } = render(
<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"
/>
);
const { getByText, queryAllByText, getAllByText } = render(<UXSection bucketSize="60s" />);
expect(getByText('User Experience')).toBeInTheDocument();
expect(getAllByText('--')).toHaveLength(3);
@ -82,17 +104,7 @@ describe('UXSection', () => {
status: fetcherHook.FETCH_STATUS.SUCCESS,
refetch: jest.fn(),
});
const { getByText, queryAllByText, getAllByText } = render(
<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"
/>
);
const { getByText, queryAllByText, getAllByText } = render(<UXSection bucketSize="60s" />);
expect(getByText('User Experience')).toBeInTheDocument();
expect(getAllByText('No data is available.')).toHaveLength(3);

View file

@ -9,28 +9,40 @@ import React from 'react';
import { SectionContainer } from '../';
import { getDataHandler } from '../../../../data_handler';
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';
interface Props {
serviceName: string;
bucketSize: string;
absoluteTime: { start?: number; end?: number };
relativeTime: { start: string; end: string };
}
export function UXSection({ serviceName, bucketSize, absoluteTime, relativeTime }: Props) {
const { start, end } = absoluteTime;
export function UXSection({ bucketSize }: Props) {
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(() => {
if (start && end) {
return getDataHandler('ux')?.fetchData({
absoluteTime: { start, end },
relativeTime,
serviceName,
bucketSize,
});
}
}, [start, end, relativeTime, serviceName, bucketSize]);
const { data, status } = useFetcher(
() => {
if (serviceName && bucketSize) {
return getDataHandler('ux')?.fetchData({
absoluteTime: { start: absoluteStart, end: absoluteEnd },
relativeTime: { start: relativeStart, end: relativeEnd },
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;

View file

@ -7,6 +7,7 @@
import { EuiSuperDatePicker } from '@elastic/eui';
import React, { useEffect } from 'react';
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 { usePluginContext } from '../../../hooks/use_plugin_context';
import { fromQuery, toQuery } from '../../../utils/url';
@ -36,6 +37,7 @@ export function DatePicker({ rangeFrom, rangeTo, refreshPaused, refreshInterval
const location = useLocation();
const history = useHistory();
const { plugins } = usePluginContext();
const { onRefreshTimeRange } = useHasData();
useEffect(() => {
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 }) {
updateUrl({ rangeFrom: start, rangeTo: end });
onRefreshTimeRange();
}
return (

View file

@ -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),
});
});
});
});

View 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}
/>
);
}

View file

@ -3,20 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
registerDataHandler,
getDataHandler,
unregisterDataHandler,
fetchHasData,
} from './data_handler';
import { registerDataHandler, getDataHandler } from './data_handler';
import moment from 'moment';
import {
ApmFetchDataResponse,
LogsFetchDataResponse,
MetricsFetchDataResponse,
UptimeFetchDataResponse,
UxFetchDataResponse,
} from './typings';
const params = {
absoluteTime: {
@ -447,203 +435,4 @@ describe('registerDataHandler', () => {
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,
});
});
});
});

View file

@ -4,11 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
DataHandler,
HasDataResponse,
ObservabilityFetchDataPlugins,
} from './typings/fetch_overview_data';
import { DataHandler, ObservabilityFetchDataPlugins } from './typings/fetch_overview_data';
const dataHandlers: Partial<Record<ObservabilityFetchDataPlugins, DataHandler>> = {};
@ -34,40 +30,3 @@ export function getDataHandler<T extends ObservabilityFetchDataPlugins>(appName:
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,
};
}

View 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);
}

View file

@ -7,7 +7,7 @@ import * as t from 'io-ts';
import { useLocation, useParams } from 'react-router-dom';
import { isLeft } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { Params } from '../routes';
import { Params, RouteParams, routes } from '../routes';
function getQueryParams(location: ReturnType<typeof useLocation>) {
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.
* @param params
*/
export function useRouteParams(params: Params) {
export function useRouteParams<T extends keyof typeof routes>(pathName: T): RouteParams<T> {
const location = useLocation();
const pathParams = useParams();
const queryParams = getQueryParams(location);
const { query, path } = routes[pathName].params as Params;
const rts = {
queryRt: params.query ? t.exact(params.query) : t.strict({}),
pathRt: params.path ? t.exact(params.path) : t.strict({}),
queryRt: query ? t.exact(query) : t.strict({}),
pathRt: path ? t.exact(path) : t.strict({}),
};
const queryResult = rts.queryRt.decode(queryParams);
@ -43,8 +44,8 @@ export function useRouteParams(params: Params) {
console.error(PathReporter.report(pathResult)[0]);
}
return {
return ({
query: isLeft(queryResult) ? {} : queryResult.right,
path: isLeft(pathResult) ? {} : pathResult.right,
};
} as unknown) as RouteParams<T>;
}

View file

@ -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(),
});
});
});
});
});

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { 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 })!,
};
}

View file

@ -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' });
});
});

View file

@ -5,33 +5,20 @@
*/
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { fetchHasData } from '../../data_handler';
import { useFetcher } from '../../hooks/use_fetcher';
import { useQueryParams } from '../../hooks/use_query_params';
import { useHasData } from '../../hooks/use_has_data';
import { LoadingObservability } from '../overview/loading_observability';
export function HomePage() {
const history = useHistory();
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;
const { hasAnyData, isAllRequestsComplete } = useHasData();
useEffect(() => {
if (hasSomeData === true) {
if (hasAnyData === true) {
history.push({ pathname: '/overview' });
}
if (hasSomeData === false) {
} else if (hasAnyData === false && isAllRequestsComplete === true) {
history.push({ pathname: '/landing' });
}
}, [hasSomeData, history]);
}, [hasAnyData, isAllRequestsComplete, history]);
return <LoadingObservability />;
}

View file

@ -4,76 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
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 { MetricsSection } from '../../components/app/section/metrics';
import { APMSection } from '../../components/app/section/apm';
import { UptimeSection } from '../../components/app/section/uptime';
import { UXSection } from '../../components/app/section/ux';
import {
HasDataResponse,
ObservabilityFetchDataPlugins,
UXHasDataResponse,
} from '../../typings/fetch_overview_data';
import { HasDataMap } from '../../context/has_data_context';
interface Props {
bucketSize: string;
absoluteTime: { start?: number; end?: number };
relativeTime: { start: string; end: string };
hasData: Record<ObservabilityFetchDataPlugins, HasDataResponse>;
hasData?: Partial<HasDataMap>;
}
export function DataSections({ bucketSize, hasData, absoluteTime, relativeTime }: Props) {
export function DataSections({ bucketSize }: Props) {
return (
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="column">
{hasData?.infra_logs && (
<EuiFlexItem grow={false}>
<LogsSection
bucketSize={bucketSize}
absoluteTime={absoluteTime}
relativeTime={relativeTime}
/>
</EuiFlexItem>
)}
{hasData?.infra_metrics && (
<EuiFlexItem grow={false}>
<MetricsSection
bucketSize={bucketSize}
absoluteTime={absoluteTime}
relativeTime={relativeTime}
/>
</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>
)}
<EuiFlexItem grow={false}>
<LogsSection bucketSize={bucketSize} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<MetricsSection bucketSize={bucketSize} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<APMSection bucketSize={bucketSize} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<UptimeSection bucketSize={bucketSize} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<UXSection bucketSize={bucketSize} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);

View file

@ -3,27 +3,25 @@
* 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, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useContext } from 'react';
import { ThemeContext } from 'styled-components';
import { useTrackPageview, UXHasDataResponse } from '../..';
import { EmptySection } from '../../components/app/empty_section';
import { useTrackPageview } from '../..';
import { Alert } from '../../../../alerts/common';
import { EmptySections } from '../../components/app/empty_sections';
import { WithHeaderLayout } from '../../components/app/layout/with_header';
import { NewsFeed } from '../../components/app/news_feed';
import { Resources } from '../../components/app/resources';
import { AlertsSection } from '../../components/app/section/alerts';
import { DatePicker, TimePickerTime } from '../../components/shared/date_picker';
import { fetchHasData } from '../../data_handler';
import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher';
import { UI_SETTINGS, useKibanaUISettings } from '../../hooks/use_kibana_ui_settings';
import { DatePicker } from '../../components/shared/date_picker';
import { useFetcher } from '../../hooks/use_fetcher';
import { useHasData } from '../../hooks/use_has_data';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { useTimeRange } from '../../hooks/use_time_range';
import { RouteParams } from '../../routes';
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 { DataSections } from './data_sections';
import { getEmptySections } from './empty_section';
import { LoadingObservability } from './loading_observability';
interface Props {
@ -37,47 +35,26 @@ function calculateBucketSize({ start, end }: { start?: number; end?: number }) {
}
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', delay: 15000 });
const { core } = usePluginContext();
const theme = useContext(ThemeContext);
const { data: alerts = [], status: alertStatus } = useFetcher(() => {
return getObservabilityAlerts({ core });
}, [core]);
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
const relativeTime = { start: relativeStart, end: relativeEnd };
const absoluteTime = { start: absoluteStart, end: absoluteEnd };
const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]);
const theme = useContext(ThemeContext);
const { hasData, hasAnyData } = useHasData();
const result = useFetcher(
() => fetchHasData(absoluteTime),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const hasData = result.data;
if (!hasData) {
if (hasAnyData === undefined) {
return <LoadingObservability />;
}
const alerts = (hasData.alert?.hasData as Alert[]) || [];
const { refreshInterval = 10000, refreshPaused = true } = routeParams.query;
const bucketSize = calculateBucketSize({
@ -85,18 +62,6 @@ export function OverviewPage({ routeParams }: Props) {
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 (
<WithHeaderLayout
headerColor={theme.eui.euiColorEmptyShade}
@ -113,42 +78,9 @@ export function OverviewPage({ routeParams }: Props) {
<EuiFlexGroup>
<EuiFlexItem grow={6}>
{/* Data sections */}
{showDataSections && (
<DataSections
hasData={hasData}
absoluteTime={absoluteTime}
relativeTime={relativeTime}
bucketSize={bucketSize?.intervalString!}
/>
)}
{hasAnyData && <DataSections bucketSize={bucketSize?.intervalString!} />}
{/* Empty sections */}
{!!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>
)}
<EmptySections />
</EuiFlexItem>
{/* Alert section */}

View file

@ -10,6 +10,7 @@ import { AppMountParameters, CoreStart } from 'kibana/public';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/public';
import { HasDataContextProvider } from '../../context/has_data_context';
import { PluginContext } from '../../context/plugin_context';
import { registerDataHandler, unregisterDataHandler } from '../../data_handler';
import { ObservabilityPluginSetupDeps } from '../../plugin';
@ -52,7 +53,9 @@ const withCore = makeDecorator({
} as unknown) as ObservabilityPluginSetupDeps,
}}
>
<EuiThemeProvider>{storyFn(context)}</EuiThemeProvider>
<EuiThemeProvider>
<HasDataContextProvider>{storyFn(context)}</HasDataContextProvider>
</EuiThemeProvider>
</PluginContext.Provider>
</MemoryRouter>
);

View file

@ -4,7 +4,7 @@
* 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';
const basePath = { prepend: (path: string) => path };
@ -27,10 +27,9 @@ describe('getObservabilityAlerts', () => {
},
basePath,
},
} as unknown) as AppMountContext['core'];
} as unknown) as CoreStart;
const alerts = await getObservabilityAlerts({ core });
expect(alerts).toEqual([]);
expect(getObservabilityAlerts({ core })).rejects.toThrow('Boom');
});
it('Returns empty array when api return undefined', async () => {
@ -43,7 +42,7 @@ describe('getObservabilityAlerts', () => {
},
basePath,
},
} as unknown) as AppMountContext['core'];
} as unknown) as CoreStart;
const alerts = await getObservabilityAlerts({ core });
expect(alerts).toEqual([]);
@ -55,32 +54,17 @@ describe('getObservabilityAlerts', () => {
get: async () => {
return {
data: [
{
id: 1,
consumer: 'siem',
},
{
id: 2,
consumer: 'kibana',
},
{
id: 3,
consumer: 'index',
},
{
id: 4,
consumer: 'foo',
},
{
id: 5,
consumer: 'bar',
},
{ id: 1, consumer: 'siem' },
{ id: 2, consumer: 'kibana' },
{ id: 3, consumer: 'index' },
{ id: 4, consumer: 'foo' },
{ id: 5, consumer: 'bar' },
],
};
},
basePath,
},
} as unknown) as AppMountContext['core'];
} as unknown) as CoreStart;
const alerts = await getObservabilityAlerts({ core });
expect(alerts).toEqual([]);
});
@ -91,36 +75,18 @@ describe('getObservabilityAlerts', () => {
get: async () => {
return {
data: [
{
id: 1,
consumer: 'siem',
},
{
id: 2,
consumer: 'apm',
},
{
id: 3,
consumer: 'uptime',
},
{
id: 4,
consumer: 'logs',
},
{
id: 5,
consumer: 'metrics',
},
{
id: 6,
consumer: 'alerts',
},
{ id: 1, consumer: 'siem' },
{ id: 2, consumer: 'apm' },
{ id: 3, consumer: 'uptime' },
{ id: 4, consumer: 'logs' },
{ id: 5, consumer: 'metrics' },
{ id: 6, consumer: 'alerts' },
],
};
},
basePath,
},
} as unknown) as AppMountContext['core'];
} as unknown) as CoreStart;
const alerts = await getObservabilityAlerts({ core });
expect(alerts).toEqual([

View file

@ -4,23 +4,24 @@
* 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';
const allowedConsumers = ['apm', 'uptime', 'logs', 'metrics', 'alerts'];
export async function getObservabilityAlerts({ core }: { core: AppMountContext['core'] }) {
export async function getObservabilityAlerts({ core }: { core: CoreStart }) {
try {
const { data = [] }: { data: Alert[] } = await core.http.get('/api/alerts/_find', {
query: {
page: 1,
per_page: 20,
},
});
const { data = [] }: { data: Alert[] } =
(await core.http.get('/api/alerts/_find', {
query: {
page: 1,
per_page: 20,
},
})) || {};
return data.filter(({ consumer }) => allowedConsumers.includes(consumer));
} catch (e) {
console.error('Error while fetching alerts', e);
return [];
throw e;
}
}

View file

@ -37,13 +37,13 @@ export interface UXHasDataResponse {
serviceName: string | number | undefined;
}
export type HasDataResponse = UXHasDataResponse | boolean;
export type FetchData<T extends FetchDataResponse = FetchDataResponse> = (
fetchDataParams: FetchDataParams
) => Promise<T>;
export type HasData = (params?: HasDataParams) => Promise<HasDataResponse>;
export type HasData<T extends ObservabilityFetchDataPlugins> = (
params?: HasDataParams
) => Promise<ObservabilityHasDataResponse[T]>;
export type ObservabilityFetchDataPlugins = Exclude<
ObservabilityApp,
@ -54,7 +54,7 @@ export interface DataHandler<
T extends ObservabilityFetchDataPlugins = ObservabilityFetchDataPlugins
> {
fetchData: FetchData<ObservabilityFetchDataResponse[T]>;
hasData: HasData;
hasData: HasData<T>;
}
export interface FetchDataResponse {
@ -113,3 +113,11 @@ export interface ObservabilityFetchDataResponse {
uptime: UptimeFetchDataResponse;
ux: UxFetchDataResponse;
}
export interface ObservabilityHasDataResponse {
apm: boolean;
infra_metrics: boolean;
infra_logs: boolean;
uptime: boolean;
ux: UXHasDataResponse;
}