Move anomaly jobs fetching to context (#94135)

* Revert "[APM] Hoist HeaderMenuPortal to prevent unmounts (#92012)"

Revert the change from #92012 to put the `HeaderMenuPortal` back within the routing context so getting data from the path params works.

Use a context to fetch the list of jobs and use it in the callout and the header menu icon. This makes it so the fetch only happens once.

A refetch function is exposed from the context and called when a new job is created.
This commit is contained in:
Nathan L Smith 2021-03-09 15:06:41 -06:00 committed by GitHub
parent 931b54f636
commit b6f8d3f8b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 108 additions and 55 deletions

View file

@ -8,7 +8,8 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import { MissingJobsAlert } from './anomaly_detection_setup_link';
import * as hooks from '../../hooks/use_fetcher';
import * as hooks from '../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
import { FETCH_STATUS } from '../../hooks/use_fetcher';
async function renderTooltipAnchor({
jobs,
@ -18,10 +19,10 @@ async function renderTooltipAnchor({
environment?: string;
}) {
// mock api response
jest.spyOn(hooks, 'useFetcher').mockReturnValue({
data: { jobs },
status: hooks.FETCH_STATUS.SUCCESS,
refetch: jest.fn(),
jest.spyOn(hooks, 'useAnomalyDetectionJobsContext').mockReturnValue({
anomalyDetectionJobsData: { jobs, hasLegacyJobs: false },
anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS,
anomalyDetectionJobsRefetch: () => {},
});
const { baseElement, container } = render(

View file

@ -18,10 +18,11 @@ import {
getEnvironmentLabel,
} from '../../../common/environment_filter_values';
import { getAPMHref } from '../../components/shared/Links/apm/APMLink';
import { useAnomalyDetectionJobsContext } from '../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context';
import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher';
import { useLicenseContext } from '../../context/license/use_license_context';
import { useUrlParams } from '../../context/url_params_context/use_url_params';
import { FETCH_STATUS } from '../../hooks/use_fetcher';
import { APIReturnType } from '../../services/rest/createCallApmApi';
import { units } from '../../style/variables';
@ -58,22 +59,18 @@ export function AnomalyDetectionSetupLink() {
}
export function MissingJobsAlert({ environment }: { environment?: string }) {
const { data = DEFAULT_DATA, status } = useFetcher(
(callApmApi) =>
callApmApi({
endpoint: `GET /api/apm/settings/anomaly-detection/jobs`,
}),
[],
{ preservePreviousData: false, showToastOnError: false }
);
const {
anomalyDetectionJobsData = DEFAULT_DATA,
anomalyDetectionJobsStatus,
} = useAnomalyDetectionJobsContext();
const defaultIcon = <EuiIcon type="inspect" color="primary" />;
if (status === FETCH_STATUS.LOADING) {
if (anomalyDetectionJobsStatus === FETCH_STATUS.LOADING) {
return <EuiLoadingSpinner />;
}
if (status !== FETCH_STATUS.SUCCESS) {
if (anomalyDetectionJobsStatus !== FETCH_STATUS.SUCCESS) {
return defaultIcon;
}
@ -81,14 +78,14 @@ export function MissingJobsAlert({ environment }: { environment?: string }) {
environment && environment !== ENVIRONMENT_ALL.value;
// there are jobs for at least one environment
if (!isEnvironmentSelected && data.jobs.length > 0) {
if (!isEnvironmentSelected && anomalyDetectionJobsData.jobs.length > 0) {
return defaultIcon;
}
// there are jobs for the selected environment
if (
isEnvironmentSelected &&
data.jobs.some((job) => environment === job.environment)
anomalyDetectionJobsData.jobs.some((job) => environment === job.environment)
) {
return defaultIcon;
}

View file

@ -13,7 +13,6 @@ import ReactDOM from 'react-dom';
import { Route, Router, Switch } from 'react-router-dom';
import 'react-vis/dist/style.css';
import { DefaultTheme, ThemeProvider } from 'styled-components';
import { HeaderMenuPortal } from '../../../observability/public';
import { euiStyled } from '../../../../../src/plugins/kibana_react/common';
import { ConfigSchema } from '../';
import { AppMountParameters, CoreStart } from '../../../../../src/core/public';
@ -36,8 +35,7 @@ import { createCallApmApi } from '../services/rest/createCallApmApi';
import { createStaticIndexPattern } from '../services/rest/index_pattern';
import { setHelpExtension } from '../setHelpExtension';
import { setReadonlyBadge } from '../updateBadge';
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
import { ActionMenu } from './action_menu';
import { AnomalyDetectionJobsContextProvider } from '../context/anomaly_detection_jobs/anomaly_detection_jobs_context';
const MainContainer = euiStyled.div`
height: 100%;
@ -45,7 +43,6 @@ const MainContainer = euiStyled.div`
function App() {
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
const { appMountParameters } = useApmPluginContext();
useBreadcrumbs(routes);
@ -58,11 +55,6 @@ function App() {
})}
>
<MainContainer data-test-subj="apmMainContainer" role="main">
<HeaderMenuPortal
setHeaderActionMenu={appMountParameters.setHeaderActionMenu}
>
<ActionMenu />
</HeaderMenuPortal>
<Route component={ScrollToTopOnPathChange} />
<Switch>
{routes.map((route, i) => (
@ -93,7 +85,9 @@ export function ApmAppRoot({
<Router history={history}>
<UrlParamsProvider>
<LicenseProvider>
<App />
<AnomalyDetectionJobsContextProvider>
<App />
</AnomalyDetectionJobsContextProvider>
</LicenseProvider>
</UrlParamsProvider>
</Router>

View file

@ -26,6 +26,7 @@ import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { createJobs } from './create_jobs';
import { getEnvironmentLabel } from '../../../../../common/environment_filter_values';
import { useAnomalyDetectionJobsContext } from '../../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
interface Props {
currentEnvironments: string[];
@ -38,6 +39,7 @@ export function AddEnvironments({
onCancel,
}: Props) {
const { notifications, application } = useApmPluginContext().core;
const { anomalyDetectionJobsRefetch } = useAnomalyDetectionJobsContext();
const canCreateJob = !!application.capabilities.ml.canCreateJob;
const { toasts } = notifications;
const { data = [], status } = useFetcher(
@ -158,6 +160,7 @@ export function AddEnvironments({
toasts,
});
if (success) {
anomalyDetectionJobsRefetch();
onCreateJobSuccess();
}
setIsSaving(false);

View file

@ -16,6 +16,8 @@ import {
import { i18n } from '@kbn/i18n';
import React, { ReactNode, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { HeaderMenuPortal } from '../../../../../observability/public';
import { ActionMenu } from '../../../application/action_menu';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { getAPMHref } from '../../shared/Links/apm/APMLink';
import { HomeLink } from '../../shared/Links/apm/HomeLink';
@ -25,7 +27,7 @@ interface SettingsProps extends RouteComponentProps<{}> {
}
export function Settings({ children, location }: SettingsProps) {
const { core } = useApmPluginContext();
const { appMountParameters, core } = useApmPluginContext();
const { basePath } = core.http;
const canAccessML = !!core.application.capabilities.ml?.canAccessML;
const { search, pathname } = location;
@ -42,6 +44,11 @@ export function Settings({ children, location }: SettingsProps) {
return (
<>
<HeaderMenuPortal
setHeaderActionMenu={appMountParameters.setHeaderActionMenu}
>
<ActionMenu />
</HeaderMenuPortal>
<EuiPage>
<EuiPageSideBar>
<HomeLink>

View file

@ -16,6 +16,7 @@ import { i18n } from '@kbn/i18n';
import React, { useEffect } from 'react';
import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
import { useTrackPageview } from '../../../../../observability/public';
import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
@ -25,7 +26,6 @@ import { SearchBar } from '../../shared/search_bar';
import { NoServicesMessage } from './no_services_message';
import { ServiceList } from './ServiceList';
import { MLCallout } from './ServiceList/MLCallout';
import { useAnomalyDetectionJobsFetcher } from './use_anomaly_detection_jobs_fetcher';
const initialData = {
items: [],
@ -108,7 +108,7 @@ export function ServiceInventory() {
const {
anomalyDetectionJobsData,
anomalyDetectionJobsStatus,
} = useAnomalyDetectionJobsFetcher();
} = useAnomalyDetectionJobsContext();
const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage(
'apm.userHasDismissedServiceInventoryMlCallout',

View file

@ -24,7 +24,7 @@ import { clearCache } from '../../../services/rest/callApi';
import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern';
import { SessionStorageMock } from '../../../services/__mocks__/SessionStorageMock';
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
import * as hook from './use_anomaly_detection_jobs_fetcher';
import * as hook from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
import { TimeRangeComparisonType } from '../../shared/time_comparison/get_time_range_comparison';
const KibanaReactContext = createKibanaReactContext({
@ -78,9 +78,10 @@ describe('ServiceInventory', () => {
global.sessionStorage = new SessionStorageMock();
clearCache();
jest.spyOn(hook, 'useAnomalyDetectionJobsFetcher').mockReturnValue({
anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS,
jest.spyOn(hook, 'useAnomalyDetectionJobsContext').mockReturnValue({
anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false },
anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS,
anomalyDetectionJobsRefetch: () => {},
});
jest

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useFetcher } from '../../../hooks/use_fetcher';
export function useAnomalyDetectionJobsFetcher() {
const { data, status } = useFetcher(
(callApmApi) =>
callApmApi({
endpoint: `GET /api/apm/settings/anomaly-detection/jobs`,
}),
[],
{ showToastOnError: false }
);
return { anomalyDetectionJobsData: data, anomalyDetectionJobsStatus: status };
}

View file

@ -8,6 +8,9 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { ReactNode } from 'react';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { HeaderMenuPortal } from '../../../../../observability/public';
import { ActionMenu } from '../../../application/action_menu';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { EnvironmentFilter } from '../EnvironmentFilter';
const HeaderFlexGroup = euiStyled(EuiFlexGroup)`
@ -17,8 +20,13 @@ const HeaderFlexGroup = euiStyled(EuiFlexGroup)`
`;
export function ApmHeader({ children }: { children: ReactNode }) {
const { setHeaderActionMenu } = useApmPluginContext().appMountParameters;
return (
<HeaderFlexGroup alignItems="center" gutterSize="s" wrap={true}>
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu}>
<ActionMenu />
</HeaderMenuPortal>
<EuiFlexItem>{children}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EnvironmentFilter />

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { createContext, ReactChild, useState } from 'react';
import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher';
import { APIReturnType } from '../../services/rest/createCallApmApi';
export interface AnomalyDetectionJobsContextValue {
anomalyDetectionJobsData?: APIReturnType<'GET /api/apm/settings/anomaly-detection/jobs'>;
anomalyDetectionJobsStatus: FETCH_STATUS;
anomalyDetectionJobsRefetch: () => void;
}
export const AnomalyDetectionJobsContext = createContext(
{} as AnomalyDetectionJobsContextValue
);
export function AnomalyDetectionJobsContextProvider({
children,
}: {
children: ReactChild;
}) {
const [fetchId, setFetchId] = useState(0);
const refetch = () => setFetchId((id) => id + 1);
const { data, status } = useFetcher(
(callApmApi) =>
callApmApi({
endpoint: `GET /api/apm/settings/anomaly-detection/jobs`,
}),
[fetchId], // eslint-disable-line react-hooks/exhaustive-deps
{ showToastOnError: false }
);
return (
<AnomalyDetectionJobsContext.Provider
value={{
anomalyDetectionJobsData: data,
anomalyDetectionJobsStatus: status,
anomalyDetectionJobsRefetch: refetch,
}}
>
{children}
</AnomalyDetectionJobsContext.Provider>
);
}

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useContext } from 'react';
import { AnomalyDetectionJobsContext } from './anomaly_detection_jobs_context';
export function useAnomalyDetectionJobsContext() {
return useContext(AnomalyDetectionJobsContext);
}