mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cases] refactoring the case view page to use react-query for case data fetching (#132928)
This commit is contained in:
parent
85638ebb88
commit
7f2c3698e9
10 changed files with 200 additions and 339 deletions
|
@ -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();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue