diff --git a/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx b/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx index 9d2ba88493774..39f22e2e988db 100644 --- a/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx +++ b/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx @@ -20,6 +20,7 @@ import type { MlJob, MlJobStats } from '@elastic/elasticsearch/lib/api/types'; import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; import usePrevious from 'react-use/lib/usePrevious'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { tz } from 'moment'; import { pick, throttle } from 'lodash'; import type { MlDependencies } from '../../application/app'; @@ -43,10 +44,17 @@ interface AppStateZoom { to?: string; } -const errorMessage = i18n.translate('xpack.ml.singleMetricViewerEmbeddable.errorMessage"', { +const basicErrorMessage = i18n.translate('xpack.ml.singleMetricViewerEmbeddable.errorMessage"', { defaultMessage: 'Unable to load the ML single metric viewer data', }); +const jobNotFoundErrorMessage = i18n.translate( + 'xpack.ml.singleMetricViewerEmbeddable.jobNotFoundErrorMessage"', + { + defaultMessage: 'No known job with the selected id', + } +); + export type SingleMetricViewerSharedComponent = FC; /** @@ -72,7 +80,7 @@ export interface SingleMetricViewerProps { */ lastRefresh?: number; onRenderComplete?: () => void; - onError?: (error: Error) => void; + onError?: (error?: Error) => void; onForecastIdChange?: (forecastId: string | undefined) => void; uuid: string; } @@ -112,6 +120,7 @@ const SingleMetricViewerWrapper: FC = ({ const [selectedJobWrapper, setSelectedJobWrapper] = useState< { job: MlJob; stats: MlJobStats } | undefined >(); + const [errorEncountered, setErrorEncountered] = useState(); const isMounted = useMountedState(); const { mlApi, mlTimeSeriesExplorerService, toastNotificationService } = mlServices; @@ -125,6 +134,16 @@ const SingleMetricViewerWrapper: FC = ({ const previousRefresh = usePrevious(lastRefresh ?? 0); + useEffect( + function resetErrorOnJobChange() { + // Calling onError to clear any previous error + setErrorEncountered(undefined); + onError?.(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [selectedJobId] + ); + useEffect( function setUpSelectedJob() { async function fetchSelectedJob() { @@ -136,19 +155,26 @@ const SingleMetricViewerWrapper: FC = ({ ]); setSelectedJobWrapper({ job: jobs[0], stats: jobStats[0] }); } catch (e) { + const error = extractErrorProperties(e); + // Could get 404 because job has been deleted and also avoid infinite refetches on any error + setErrorEncountered(error.statusCode); if (onError) { - onError(new Error(errorMessage)); + onError( + new Error(errorEncountered === 404 ? jobNotFoundErrorMessage : basicErrorMessage) + ); } } } } - if (isMounted() === false) { + if (isMounted() === false || errorEncountered !== undefined) { return; } fetchSelectedJob(); }, - [selectedJobId, mlApi, isMounted, onError] + // eslint-disable-next-line react-hooks/exhaustive-deps + [selectedJobId, isMounted, errorEncountered] ); + // eslint-disable-next-line react-hooks/exhaustive-deps const resizeHandler = useCallback( throttle((e: { width: number; height: number }) => {