[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,
|
||||
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: {
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: () => {} },
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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', () => {
|
|
@ -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 { 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();
|
||||
|
|
|
@ -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 }}
|
||||
/>
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
* 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
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 { 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>;
|
||||
}
|
||||
|
|
|
@ -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 { 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 />;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue