From 339ce82fd658e006c3a37f3829d1b5feeb78be4e Mon Sep 17 00:00:00 2001 From: YanJin Date: Fri, 21 Apr 2023 11:16:23 +0200 Subject: [PATCH] ZKUI-348: Polling to update the pause and resume button --- src/react/backend/location/PauseAndResume.tsx | 159 +++++++++--------- .../__tests__/PauseAndResume.test.tsx | 102 ++++++++++- 2 files changed, 183 insertions(+), 78 deletions(-) diff --git a/src/react/backend/location/PauseAndResume.tsx b/src/react/backend/location/PauseAndResume.tsx index 59293afff..bb7cf431e 100644 --- a/src/react/backend/location/PauseAndResume.tsx +++ b/src/react/backend/location/PauseAndResume.tsx @@ -1,86 +1,93 @@ import { Icon, Loader, spacing } from '@scality/core-ui'; import { Box } from '@scality/core-ui/dist/next'; -import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { useMemo, useRef, useState } from 'react'; +import { useMutation, useQuery } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from '../../../types/state'; import { notFalsyTypeGuard } from '../../../types/typeGuards'; -import { networkEnd, networkStart } from '../../actions'; import { useManagementClient } from '../../ManagementProvider'; import { InlineButton } from '../../ui-elements/Table'; import { getInstanceStatusQuery } from './queries'; export const PauseAndResume = ({ locationName }: { locationName: string }) => { + const [isPollingEnabled, setIsPollingEnabled] = useState(false); + const previousStatusRef = useRef<{ + replication: 'enabled' | 'disabled' | null; + ingestion: 'enabled' | 'disabled' | null; + } | null>(null); const dispatch = useDispatch(); const instanceId = notFalsyTypeGuard( useSelector((state: AppState) => state.instances.selectedId), ); const managementClient = useManagementClient(); - const queryClient = useQueryClient(); const instanceStatusQuery = getInstanceStatusQuery( dispatch, notFalsyTypeGuard(managementClient), instanceId, ); - const invalidateInstanceStatusQueryCache = async () => { - await queryClient.refetchQueries(instanceStatusQuery.queryKey); - dispatch(networkEnd()); - }; const zenkoClient = useSelector((state: AppState) => state.zenko.zenkoClient); - const pauseReplicationSiteMutation = useMutation( - (locationName: string) => { - dispatch(networkStart('Pausing Async Metadata updates')); - return zenkoClient.pauseCrrSite(locationName); - }, - { - onSuccess: () => { - invalidateInstanceStatusQueryCache(); - }, - onError: () => { - dispatch(networkEnd()); - }, - }, - ); + const pauseReplicationSiteMutation = useMutation((locationName: string) => { + return zenkoClient.pauseCrrSite(locationName); + }); - const resumeReplicationSiteMutation = useMutation( - (locationName: string) => { - dispatch(networkStart('Resuming Replication workflow')); - return zenkoClient.resumeCrrSite(locationName); - }, - { - onSuccess: () => { - invalidateInstanceStatusQueryCache(); - }, - onError: () => { - dispatch(networkEnd()); - }, - }, - ); + const resumeReplicationSiteMutation = useMutation((locationName: string) => { + return zenkoClient.resumeCrrSite(locationName); + }); - const pauseIngestionSiteMutation = useMutation( - (locationName: string) => zenkoClient.pauseIngestionSite(locationName), - { - onSuccess: () => { - invalidateInstanceStatusQueryCache(); - }, - }, + const pauseIngestionSiteMutation = useMutation((locationName: string) => + zenkoClient.pauseIngestionSite(locationName), ); - const resumeIngestionSiteMutation = useMutation( - (locationName: string) => zenkoClient.resumeIngestionSite(locationName), - { - onSuccess: () => { - invalidateInstanceStatusQueryCache(); - }, - }, + const resumeIngestionSiteMutation = useMutation((locationName: string) => + zenkoClient.resumeIngestionSite(locationName), ); const { data: instanceStatus, status, - isFetching: loading, - } = useQuery(instanceStatusQuery); + isFetching: loadingWorkflowStatus, + } = useQuery({ + ...instanceStatusQuery, + refetchInterval: isPollingEnabled ? 1_000 : Infinity, + }); + + const ingestionLocationsStatuses = + instanceStatus?.metrics?.['ingest-schedule']?.states; + const replicationLocationsStatuses = + instanceStatus?.metrics?.['crr-schedule']?.states; + const ingestionStatus = + (ingestionLocationsStatuses && ingestionLocationsStatuses[locationName]) || + null; + + const replicationStatus = + (replicationLocationsStatuses && + replicationLocationsStatuses[locationName]) || + null; + + //the previous replication or ingestion could be null, so we should ignore it while computing the polling. + useMemo(() => { + if ( + previousStatusRef.current && + previousStatusRef.current.ingestion !== ingestionStatus && + previousStatusRef.current.replication !== replicationStatus + ) { + setIsPollingEnabled(false); + } else if ( + previousStatusRef.current && + previousStatusRef.current.ingestion !== ingestionStatus && + previousStatusRef.current.replication === null + ) { + setIsPollingEnabled(false); + } else if ( + previousStatusRef.current && + previousStatusRef.current.replication !== ingestionStatus && + previousStatusRef.current.ingestion === null + ) { + setIsPollingEnabled(false); + } + }, [replicationStatus, ingestionStatus]); if (status === 'loading' || status === 'idle') { return ( @@ -90,45 +97,40 @@ export const PauseAndResume = ({ locationName }: { locationName: string }) => { ); } - const ingestionStates = instanceStatus?.metrics?.['ingest-schedule']?.states; - const replicationStates = instanceStatus?.metrics?.['crr-schedule']?.states; - const ingestion = - ingestionStates && - ingestionStates[locationName] && - ingestionStates[locationName]; - - const replication = - replicationStates && replicationStates[locationName] - ? replicationStates[locationName] - : null; + const isLoadingButton = loadingWorkflowStatus || isPollingEnabled; const tooltip = ( - {replication === 'enabled' && 'Replication workflow is active.'} - {ingestion === 'enabled' && 'Async Metadata updates is active.'} - {replication === 'disabled' && 'Replication workflow is paused.'} - {ingestion === 'disabled' && 'Async Metadata updates is paused.'} + {replicationStatus === 'enabled' && 'Replication workflow is active.'} + {ingestionStatus === 'enabled' && 'Async Metadata updates is active.'} + {replicationStatus === 'disabled' && 'Replication workflow is paused.'} + {ingestionStatus === 'disabled' && 'Async Metadata updates is paused.'} ); - if (replication === 'enabled' || ingestion === 'enabled') { + if (replicationStatus === 'enabled' || ingestionStatus === 'enabled') { return ( } + disabled={isLoadingButton} + icon={isLoadingButton ? : } tooltip={{ overlay: tooltip, placement: 'top', }} label="Pause" onClick={() => { - if (replication === 'enabled') { + previousStatusRef.current = { + replication: replicationStatus, + ingestion: ingestionStatus, + }; + if (replicationStatus === 'enabled') { pauseReplicationSiteMutation.mutate(locationName); } - if (ingestion === 'enabled') { + if (ingestionStatus === 'enabled') { pauseIngestionSiteMutation.mutate(locationName); } + setIsPollingEnabled(true); }} variant="secondary" type="button" @@ -137,12 +139,12 @@ export const PauseAndResume = ({ locationName }: { locationName: string }) => { ); } - if (replication === 'disabled' || ingestion === 'disabled') { + if (replicationStatus === 'disabled' || ingestionStatus === 'disabled') { return ( } + disabled={isLoadingButton} + icon={isLoadingButton ? : } tooltip={{ overlay: tooltip, placement: 'top', @@ -150,12 +152,17 @@ export const PauseAndResume = ({ locationName }: { locationName: string }) => { type="button" label="Resume" onClick={() => { - if (replication === 'disabled') { + previousStatusRef.current = { + replication: replicationStatus, + ingestion: ingestionStatus, + }; + if (replicationStatus === 'disabled') { resumeReplicationSiteMutation.mutate(locationName); } - if (ingestion === 'disabled') { + if (ingestionStatus === 'disabled') { resumeIngestionSiteMutation.mutate(locationName); } + setIsPollingEnabled(true); }} variant="secondary" /> diff --git a/src/react/backend/location/__tests__/PauseAndResume.test.tsx b/src/react/backend/location/__tests__/PauseAndResume.test.tsx index 4e29f23c0..7567aaf6f 100644 --- a/src/react/backend/location/__tests__/PauseAndResume.test.tsx +++ b/src/react/backend/location/__tests__/PauseAndResume.test.tsx @@ -1,4 +1,9 @@ -import { screen, waitForElementToBeRemoved } from '@testing-library/react'; +import { + screen, + waitFor, + waitForElementToBeRemoved, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { reduxRender, TEST_API_BASE_URL } from '../../../utils/testUtil'; @@ -7,7 +12,16 @@ import { PauseAndResume } from '../PauseAndResume'; describe('PauseAndResume', () => { const instanceId = 'instanceId'; const locationName = 'someLocation'; - const server = setupServer(); + const server = setupServer( + rest.post( + `${TEST_API_BASE_URL}/_/backbeat/api/crr/pause/someLocation`, + (req, res, ctx) => res(ctx.status(200)), + ), + rest.post( + `${TEST_API_BASE_URL}/_/backbeat/api/ingestion/pause/someLocation`, + (req, res, ctx) => res(ctx.status(200)), + ), + ); beforeAll(() => { server.listen({ onUnhandledRequest: 'error' }); @@ -138,6 +152,90 @@ describe('PauseAndResume', () => { expect(screen.getByRole('button', { name: /Pause/i })).not.toBeDisabled(); }); + [ + { ingestionStatus: 'enabled', replicationStatus: 'enabled' }, + { ingestionStatus: null, replicationStatus: 'enabled' }, + { ingestionStatus: 'enabled', replicationStatus: null }, + ].forEach((testCase) => { + it(`should disable the pause button while performing the action and then resolve with a resume button for {ingestionStatus: ${testCase.ingestionStatus}, replicationStatus: ${testCase.replicationStatus}}`, async () => { + //S + server.use( + rest.get( + `${TEST_API_BASE_URL}/api/v1/instance/${instanceId}/status`, + (req, res, ctx) => + res( + ctx.json({ + metrics: { + ['ingest-schedule']: testCase.ingestionStatus + ? { + states: { [locationName]: testCase.ingestionStatus }, + } + : {}, + ['crr-schedule']: testCase.replicationStatus + ? { states: { [locationName]: testCase.replicationStatus } } + : {}, + }, + state: null, + }), + ), + ), + ); + + const pauseButtonSelector = () => + screen.getByRole('button', { name: /Pause/i }); + const resumeButtonSelector = () => + screen.getByRole('button', { name: /resume/i }); + + reduxRender(, { + instances: { + selectedId: instanceId, + }, + }); + + await waitForElementToBeRemoved(() => screen.queryByText('Loading'), { + timeout: 8000, + }); + + expect(pauseButtonSelector()).toBeInTheDocument(); + expect(pauseButtonSelector()).not.toBeDisabled(); + + //E + userEvent.click(pauseButtonSelector()); + + //V + expect(pauseButtonSelector()).toBeDisabled(); + + //S + server.use( + rest.get( + `${TEST_API_BASE_URL}/api/v1/instance/${instanceId}/status`, + (req, res, ctx) => { + return res( + ctx.json({ + metrics: { + ['ingest-schedule']: testCase.ingestionStatus + ? { + states: { [locationName]: 'disabled' }, + } + : {}, + ['crr-schedule']: testCase.replicationStatus + ? { states: { [locationName]: 'disabled' } } + : {}, + }, + state: null, + }), + ); + }, + ), + ); + + //E + await waitFor(() => resumeButtonSelector(), { timeout: 2000 }); + //V + expect(resumeButtonSelector()).not.toBeDisabled(); + }); + }); + it('should render the component with resume label when ingestion is disabled', async () => { server.use( rest.get(