[Cases] refactoring the case view page to use react-query for case data fetching (#132928)

This commit is contained in:
Esteban Beltran 2022-05-30 10:12:40 +02:00 committed by GitHub
parent 85638ebb88
commit 7f2c3698e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 200 additions and 339 deletions

View file

@ -8,7 +8,7 @@
import React from 'react';
// eslint-disable-next-line @kbn/eslint/module_migration
import { MemoryRouterProps } from 'react-router';
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { TestProviders } from '../../common/mock';
import { CasesRoutes } from './routes';
@ -17,10 +17,6 @@ jest.mock('../all_cases', () => ({
AllCases: () => <div>{'All cases'}</div>,
}));
jest.mock('../case_view', () => ({
CaseView: () => <div>{'Case view'}</div>,
}));
jest.mock('../create', () => ({
CreateCase: () => <div>{'Create case'}</div>,
}));
@ -59,17 +55,25 @@ describe('Cases routes', () => {
});
describe('Case view', () => {
it.each(getCaseViewPaths())('navigates to the cases view page for path: %s', (path: string) => {
renderWithRouter([path]);
expect(screen.getByText('Case view')).toBeInTheDocument();
// User has read only privileges
});
it.each(getCaseViewPaths())(
'navigates to the cases view page for path: %s',
async (path: string) => {
renderWithRouter([path]);
await waitFor(() => {
expect(screen.getByTestId('case-view-loading')).toBeInTheDocument();
});
// User has read only privileges
}
);
it.each(getCaseViewPaths())(
'user can navigate to the cases view page with userCanCrud = false and path: %s',
(path: string) => {
async (path: string) => {
renderWithRouter([path], false);
expect(screen.getByText('Case view')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByTestId('case-view-loading')).toBeInTheDocument();
});
}
);
});

View file

@ -5,11 +5,12 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import React, { lazy, Suspense, useCallback } from 'react';
import { Redirect, Switch } from 'react-router-dom';
import { Route } from '@kbn/kibana-react-plugin/public';
import { QueryClientProvider } from 'react-query';
import { EuiLoadingSpinner } from '@elastic/eui';
import { AllCases } from '../all_cases';
import { CaseView } from '../case_view';
import { CreateCase } from '../create';
import { ConfigureCases } from '../configure_cases';
import { CasesRoutesProps } from './types';
@ -25,6 +26,10 @@ import {
import { NoPrivilegesPage } from '../no_privileges';
import * as i18n from './translations';
import { useReadonlyHeader } from './use_readonly_header';
import { casesQueryClient } from '../cases_context/query_client';
import type { CaseViewProps } from '../case_view/types';
const CaseViewLazy: React.FC<CaseViewProps> = lazy(() => import('../case_view'));
const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
onComponentInitialized,
@ -46,47 +51,51 @@ const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
);
return (
<Switch>
<Route strict exact path={basePath}>
<AllCases />
</Route>
<QueryClientProvider client={casesQueryClient}>
<Switch>
<Route strict exact path={basePath}>
<AllCases />
</Route>
<Route path={getCreateCasePath(basePath)}>
{userCanCrud ? (
<CreateCase
onSuccess={onCreateCaseSuccess}
onCancel={navigateToAllCases}
timelineIntegration={timelineIntegration}
/>
) : (
<NoPrivilegesPage pageName={i18n.CREATE_CASE_PAGE_NAME} />
)}
</Route>
<Route path={getCreateCasePath(basePath)}>
{userCanCrud ? (
<CreateCase
onSuccess={onCreateCaseSuccess}
onCancel={navigateToAllCases}
timelineIntegration={timelineIntegration}
/>
) : (
<NoPrivilegesPage pageName={i18n.CREATE_CASE_PAGE_NAME} />
)}
</Route>
<Route path={getCasesConfigurePath(basePath)}>
{userCanCrud ? (
<ConfigureCases />
) : (
<NoPrivilegesPage pageName={i18n.CONFIGURE_CASES_PAGE_NAME} />
)}
</Route>
<Route path={getCasesConfigurePath(basePath)}>
{userCanCrud ? (
<ConfigureCases />
) : (
<NoPrivilegesPage pageName={i18n.CONFIGURE_CASES_PAGE_NAME} />
)}
</Route>
<Route exact path={[getCaseViewWithCommentPath(basePath), getCaseViewPath(basePath)]}>
<CaseView
onComponentInitialized={onComponentInitialized}
actionsNavigation={actionsNavigation}
ruleDetailsNavigation={ruleDetailsNavigation}
showAlertDetails={showAlertDetails}
useFetchAlertData={useFetchAlertData}
refreshRef={refreshRef}
timelineIntegration={timelineIntegration}
/>
</Route>
<Route exact path={[getCaseViewWithCommentPath(basePath), getCaseViewPath(basePath)]}>
<Suspense fallback={<EuiLoadingSpinner />}>
<CaseViewLazy
onComponentInitialized={onComponentInitialized}
actionsNavigation={actionsNavigation}
ruleDetailsNavigation={ruleDetailsNavigation}
showAlertDetails={showAlertDetails}
useFetchAlertData={useFetchAlertData}
refreshRef={refreshRef}
timelineIntegration={timelineIntegration}
/>
</Suspense>
</Route>
<Route path={basePath}>
<Redirect to={basePath} />
</Route>
</Switch>
<Route path={basePath}>
<Redirect to={basePath} />
</Route>
</Switch>
</QueryClientProvider>
);
};
CasesRoutesComponent.displayName = 'CasesRoutes';

View file

@ -111,7 +111,7 @@ export const CaseViewPage = React.memo<CaseViewPageProps>(
if (isStale || isLoading || isLoadingMetrics || isLoadingUserActions) {
return;
}
await Promise.all([fetchCase(true), fetchCaseMetrics(true), refetchCaseUserActions()]);
await Promise.all([fetchCase(), fetchCaseMetrics(true), refetchCaseUserActions()]);
},
};
return () => {

View file

@ -43,7 +43,7 @@ jest.mock('../user_actions/timestamp');
jest.mock('../../common/lib/kibana');
jest.mock('../../common/navigation/hooks');
const useGetCaseMock = useGetCase as jest.Mock;
const useFetchCaseMock = useGetCase as jest.Mock;
const useGetCaseMetricsMock = useGetCaseMetrics as jest.Mock;
const useUpdateCaseMock = useUpdateCase as jest.Mock;
const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock;
@ -119,18 +119,18 @@ export const caseData: Case = {
describe('CaseView', () => {
const updateCaseProperty = jest.fn();
const fetchCaseUserActions = jest.fn();
const fetchCase = jest.fn();
const refetchCase = jest.fn();
const fetchCaseMetrics = jest.fn();
const updateCase = jest.fn();
const pushCaseToExternalService = jest.fn();
const defaultGetCase = {
isLoading: false,
isError: false,
data: caseData,
resolveOutcome: 'exactMatch',
updateCase,
fetchCase,
data: {
case: caseData,
outcome: 'exactMatch',
},
refetch: refetchCase,
};
const defaultGetCaseMetrics = {
@ -160,7 +160,15 @@ describe('CaseView', () => {
};
const mockGetCase = (props: Partial<UseGetCase> = {}) => {
useGetCaseMock.mockReturnValue({ ...defaultGetCase, ...props });
const data = {
...defaultGetCase.data,
...props.data,
};
useFetchCaseMock.mockReturnValue({
...defaultGetCase,
...props,
data,
});
};
beforeAll(() => {
@ -202,7 +210,7 @@ describe('CaseView', () => {
});
it('should return case view when data is there', async () => {
mockGetCase({ resolveOutcome: 'exactMatch' });
mockGetCase({ data: { ...defaultGetCase.data, outcome: 'exactMatch' } });
const wrapper = mount(
<TestProviders>
<CaseView {...caseViewProps} />
@ -216,9 +224,16 @@ describe('CaseView', () => {
});
it('should redirect case view when resolves to alias match', async () => {
const resolveAliasId = `${defaultGetCase.data.id}_2`;
const resolveAliasId = `${defaultGetCase.data.case.id}_2`;
const resolveAliasPurpose = 'savedObjectConversion' as const;
mockGetCase({ resolveOutcome: 'aliasMatch', resolveAliasId, resolveAliasPurpose });
mockGetCase({
data: {
...defaultGetCase.data,
outcome: 'aliasMatch',
aliasTargetId: resolveAliasId,
aliasPurpose: resolveAliasPurpose,
},
});
const wrapper = mount(
<TestProviders>
<CaseView {...caseViewProps} />
@ -236,8 +251,10 @@ describe('CaseView', () => {
});
it('should redirect case view when resolves to conflict', async () => {
const resolveAliasId = `${defaultGetCase.data.id}_2`;
mockGetCase({ resolveOutcome: 'conflict', resolveAliasId });
const resolveAliasId = `${defaultGetCase.data.case.id}_2`;
mockGetCase({
data: { ...defaultGetCase.data, outcome: 'conflict', aliasTargetId: resolveAliasId },
});
const wrapper = mount(
<TestProviders>
<CaseView {...caseViewProps} />
@ -249,7 +266,7 @@ describe('CaseView', () => {
expect(spacesUiApiMock.redirectLegacyUrl).not.toHaveBeenCalled();
expect(spacesUiApiMock.components.getLegacyUrlConflict).toHaveBeenCalledWith({
objectNoun: 'case',
currentObjectId: defaultGetCase.data.id,
currentObjectId: defaultGetCase.data.case.id,
otherObjectId: resolveAliasId,
otherObjectPath: `/cases/${resolveAliasId}`,
});
@ -257,7 +274,6 @@ describe('CaseView', () => {
});
it('should refresh data on refresh', async () => {
(useGetCase as jest.Mock).mockImplementation(() => defaultGetCase);
const wrapper = mount(
<TestProviders>
<CaseView {...caseViewProps} />
@ -267,7 +283,7 @@ describe('CaseView', () => {
await waitFor(() => {
expect(fetchCaseUserActions).toBeCalledWith(caseData.id, 'resilient-2');
expect(fetchCaseMetrics).toBeCalled();
expect(fetchCase).toBeCalled();
expect(refetchCase).toBeCalled();
});
});
@ -275,7 +291,6 @@ describe('CaseView', () => {
let refreshRef: CaseViewProps['refreshRef'];
beforeEach(async () => {
(useGetCase as jest.Mock).mockImplementation(() => defaultGetCase);
refreshRef = React.createRef();
await act(async () => {
@ -307,7 +322,7 @@ describe('CaseView', () => {
await waitFor(() => {
expect(fetchCaseUserActions).toBeCalledWith('basic-case-id', 'resilient-2');
expect(fetchCaseMetrics).toBeCalledWith(true);
expect(fetchCase).toBeCalledWith(true);
expect(refetchCase).toHaveBeenCalled();
});
});
});

View file

@ -18,6 +18,7 @@ import { useCasesContext } from '../cases_context/use_cases_context';
import { generateCaseViewPath, useCaseViewParams } from '../../common/navigation';
import { CaseViewPage } from './case_view_page';
import type { CaseViewProps } from './types';
import { Case } from '../../containers/types';
const MyEuiFlexGroup = styled(EuiFlexGroup)`
height: 100%;
@ -44,70 +45,64 @@ export const CaseView = React.memo(
const { spaces: spacesApi } = useKibana().services;
const { detailName: caseId } = useCaseViewParams();
const { basePath } = useCasesContext();
const {
data,
resolveOutcome,
resolveAliasId,
resolveAliasPurpose,
isLoading,
isError,
fetchCase,
updateCase,
} = useGetCase(caseId);
const { data, isLoading, isError, refetch } = useGetCase(caseId);
const updateCase = (_newCase: Case) => {
refetch();
};
useEffect(() => {
if (spacesApi && resolveOutcome === 'aliasMatch' && resolveAliasId != null) {
const newPath = `${basePath}${generateCaseViewPath({ detailName: resolveAliasId })}`;
if (spacesApi && data?.outcome === 'aliasMatch' && data.aliasTargetId != null) {
const newPath = `${basePath}${generateCaseViewPath({ detailName: data.aliasTargetId })}`;
spacesApi.ui.redirectLegacyUrl({
path: `${newPath}${window.location.search}${window.location.hash}`,
aliasPurpose: resolveAliasPurpose,
aliasPurpose: data.aliasPurpose,
objectNoun: i18n.CASE,
});
}
}, [resolveOutcome, resolveAliasId, resolveAliasPurpose, basePath, spacesApi]);
}, [basePath, data, spacesApi]);
const getLegacyUrlConflictCallout = useCallback(() => {
// This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario
if (data && spacesApi && resolveOutcome === 'conflict' && resolveAliasId != null) {
if (data && spacesApi && data.outcome === 'conflict' && data.aliasTargetId != null) {
// We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a
// callout with a warning for the user, and provide a way for them to navigate to the other object.
const otherObjectPath = `${basePath}${generateCaseViewPath({
detailName: resolveAliasId,
detailName: data.aliasTargetId,
})}${window.location.search}${window.location.hash}`;
return spacesApi.ui.components.getLegacyUrlConflict({
objectNoun: i18n.CASE,
currentObjectId: data.id,
otherObjectId: resolveAliasId,
currentObjectId: data.case.id,
otherObjectId: data.aliasTargetId,
otherObjectPath,
});
}
return null;
}, [data, resolveAliasId, resolveOutcome, basePath, spacesApi]);
}, [basePath, data, spacesApi]);
return isError ? (
<DoesNotExist caseId={caseId} />
) : isLoading ? (
<CaseViewLoading />
) : (
data && (
<CasesTimelineIntegrationProvider timelineIntegration={timelineIntegration}>
{getLegacyUrlConflictCallout()}
<CaseViewPage
caseData={data}
caseId={caseId}
fetchCase={fetchCase}
onComponentInitialized={onComponentInitialized}
actionsNavigation={actionsNavigation}
ruleDetailsNavigation={ruleDetailsNavigation}
showAlertDetails={showAlertDetails}
updateCase={updateCase}
useFetchAlertData={useFetchAlertData}
refreshRef={refreshRef}
/>
</CasesTimelineIntegrationProvider>
)
);
) : data ? (
<CasesTimelineIntegrationProvider timelineIntegration={timelineIntegration}>
{getLegacyUrlConflictCallout()}
<CaseViewPage
caseData={data.case}
caseId={caseId}
fetchCase={refetch}
onComponentInitialized={onComponentInitialized}
actionsNavigation={actionsNavigation}
ruleDetailsNavigation={ruleDetailsNavigation}
showAlertDetails={showAlertDetails}
updateCase={updateCase}
useFetchAlertData={useFetchAlertData}
refreshRef={refreshRef}
/>
</CasesTimelineIntegrationProvider>
) : null;
}
);

View file

@ -8,7 +8,6 @@ import { MutableRefObject } from 'react';
import { CasesTimelineIntegration } from '../timeline_context';
import { CasesNavigation } from '../links';
import { CaseViewRefreshPropInterface, Case } from '../../../common';
import { UseGetCase } from '../../containers/use_get_case';
import { UseFetchAlertData } from '../../../common/ui';
export interface CaseViewBaseProps {
@ -30,7 +29,7 @@ export interface CaseViewProps extends CaseViewBaseProps {
export interface CaseViewPageProps extends CaseViewBaseProps {
caseId: string;
fetchCase: UseGetCase['fetchCase'];
fetchCase: () => void;
caseData: Case;
updateCase: (newCase: Case) => void;
}

View file

@ -0,0 +1,10 @@
/*
* 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 { QueryClient } from 'react-query';
export const casesQueryClient = new QueryClient();

View file

@ -5,127 +5,45 @@
* 2.0.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useGetCase, UseGetCase } from './use_get_case';
import { basicCase, basicResolvedCase } from './mock';
import { renderHook } from '@testing-library/react-hooks';
import { useGetCase } from './use_get_case';
import * as api from './api';
import { waitFor } from '@testing-library/dom';
import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { useToasts } from '../common/lib/kibana';
jest.mock('./api');
jest.mock('../common/lib/kibana');
describe('useGetCase', () => {
const abortCtrl = new AbortController();
beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
const wrapper: React.FC<string> = ({ children }) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
};
describe('Use get case hook', () => {
it('calls the api when invoked with the correct parameters', async () => {
const spy = jest.spyOn(api, 'resolveCase');
const { waitForNextUpdate } = renderHook(() => useGetCase('case-1'), { wrapper });
await waitForNextUpdate();
expect(spy).toHaveBeenCalledWith('case-1', true, expect.any(AbortSignal));
});
it('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() =>
useGetCase(basicCase.id)
);
await waitForNextUpdate();
expect(result.current).toEqual({
data: null,
resolveOutcome: null,
isLoading: false,
isError: false,
fetchCase: result.current.fetchCase,
updateCase: result.current.updateCase,
});
});
});
it('calls resolveCase with correct arguments', async () => {
const spyOnResolveCase = jest.spyOn(api, 'resolveCase');
await act(async () => {
const { waitForNextUpdate } = renderHook<string, UseGetCase>(() => useGetCase(basicCase.id));
await waitForNextUpdate();
await waitForNextUpdate();
expect(spyOnResolveCase).toBeCalledWith(basicCase.id, true, abortCtrl.signal);
});
});
it('fetch case', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() =>
useGetCase(basicCase.id)
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
data: basicCase,
resolveOutcome: basicResolvedCase.outcome,
resolveAliasId: basicResolvedCase.aliasTargetId,
isLoading: false,
isError: false,
fetchCase: result.current.fetchCase,
updateCase: result.current.updateCase,
});
});
});
it('refetch case', async () => {
const spyOnResolveCase = jest.spyOn(api, 'resolveCase');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() =>
useGetCase(basicCase.id)
);
await waitForNextUpdate();
await waitForNextUpdate();
result.current.fetchCase();
expect(spyOnResolveCase).toHaveBeenCalledTimes(2);
});
});
it('set isLoading to true when refetching case', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() =>
useGetCase(basicCase.id)
);
await waitForNextUpdate();
await waitForNextUpdate();
result.current.fetchCase();
expect(result.current.isLoading).toBe(true);
});
});
it('set isLoading to false when refetching case "silent"ly', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() =>
useGetCase(basicCase.id)
);
await waitForNextUpdate();
await waitForNextUpdate();
result.current.fetchCase(true);
expect(result.current.isLoading).toBe(false);
});
});
it('unhappy path', async () => {
const spyOnResolveCase = jest.spyOn(api, 'resolveCase');
spyOnResolveCase.mockImplementation(() => {
throw new Error('Something went wrong');
});
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() =>
useGetCase(basicCase.id)
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
data: null,
resolveOutcome: null,
isLoading: false,
isError: true,
fetchCase: result.current.fetchCase,
updateCase: result.current.updateCase,
});
it('shows a toast error when the api return an error', async () => {
const addError = jest.fn();
(useToasts as jest.Mock).mockReturnValue({ addError });
const spy = jest.spyOn(api, 'resolveCase').mockRejectedValue(new Error("C'est la vie"));
const { waitForNextUpdate } = renderHook(() => useGetCase('case-1'), { wrapper });
await waitForNextUpdate();
await waitFor(() => {
expect(spy).toHaveBeenCalledWith('case-1', true, expect.any(AbortSignal));
expect(addError).toHaveBeenCalled();
});
});
});

View file

@ -5,125 +5,34 @@
* 2.0.
*/
import { useEffect, useReducer, useCallback, useRef } from 'react';
import { Case, ResolvedCase } from './types';
import { useQuery } from 'react-query';
import { ResolvedCase } from './types';
import * as i18n from './translations';
import { useToasts } from '../common/lib/kibana';
import { resolveCase } from './api';
import { ServerError } from '../types';
interface CaseState {
data: Case | null;
resolveOutcome: ResolvedCase['outcome'] | null;
resolveAliasId?: ResolvedCase['aliasTargetId'];
resolveAliasPurpose?: ResolvedCase['aliasPurpose'];
isLoading: boolean;
isError: boolean;
}
type Action =
| { type: 'FETCH_INIT'; payload: { silent: boolean } }
| { type: 'FETCH_SUCCESS'; payload: ResolvedCase }
| { type: 'FETCH_FAILURE' }
| { type: 'UPDATE_CASE'; payload: Case };
const dataFetchReducer = (state: CaseState, action: Action): CaseState => {
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
// If doing a silent fetch, then don't set `isLoading`. This helps
// with preventing screen flashing when wanting to refresh the actions
// and comments
isLoading: !action.payload?.silent,
isError: false,
};
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: action.payload.case,
resolveOutcome: action.payload.outcome,
resolveAliasId: action.payload.aliasTargetId,
resolveAliasPurpose: action.payload.aliasPurpose,
};
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
};
case 'UPDATE_CASE':
return {
...state,
data: action.payload,
};
default:
return state;
}
};
export interface UseGetCase extends CaseState {
/**
* @param [silent] When set to `true`, the `isLoading` property will not be set to `true`
* while doing the API call
*/
fetchCase: (silent?: boolean) => Promise<void>;
updateCase: (newCase: Case) => void;
}
export const useGetCase = (caseId: string): UseGetCase => {
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: null,
resolveOutcome: null,
});
export const useGetCase = (caseId: string) => {
const toasts = useToasts();
const isCancelledRef = useRef(false);
const abortCtrlRef = useRef(new AbortController());
const updateCase = useCallback((newCase: Case) => {
dispatch({ type: 'UPDATE_CASE', payload: newCase });
}, []);
const callFetch = useCallback(
async (silent: boolean = false) => {
try {
isCancelledRef.current = false;
abortCtrlRef.current.abort();
abortCtrlRef.current = new AbortController();
dispatch({ type: 'FETCH_INIT', payload: { silent } });
const response: ResolvedCase = await resolveCase(caseId, true, abortCtrlRef.current.signal);
if (!isCancelledRef.current) {
dispatch({ type: 'FETCH_SUCCESS', payload: response });
}
} catch (error) {
if (!isCancelledRef.current) {
if (error.name !== 'AbortError') {
toasts.addError(
error.body && error.body.message ? new Error(error.body.message) : error,
{ title: i18n.ERROR_TITLE }
);
}
dispatch({ type: 'FETCH_FAILURE' });
}
}
return useQuery<ResolvedCase, ServerError>(
['case', caseId],
() => {
const abortCtrlRef = new AbortController();
return resolveCase(caseId, true, abortCtrlRef.signal);
},
[caseId, toasts]
{
onError: (error: ServerError) => {
if (error.name !== 'AbortError') {
toasts.addError(
error.body && error.body.message ? new Error(error.body.message) : error,
{
title: i18n.ERROR_TITLE,
}
);
}
},
}
);
useEffect(() => {
callFetch();
return () => {
isCancelledRef.current = true;
abortCtrlRef.current.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [caseId]);
return { ...state, fetchCase: callFetch, updateCase };
};
export type UseGetCase = ReturnType<typeof useGetCase>;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { CoreStart } from '@kbn/core/public';
import { CoreStart, IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { ReactElement, ReactNode } from 'react';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
@ -136,3 +136,5 @@ export interface CasesUiStart {
export type SupportedCaseAttachment = CommentRequestAlertType | CommentRequestUserType;
export type CaseAttachments = SupportedCaseAttachment[];
export type ServerError = IHttpFetchError<ResponseErrorBody>;