Skip to content

Commit

Permalink
ZKUI-348: Polling to update the pause and resume button
Browse files Browse the repository at this point in the history
  • Loading branch information
ChengYanJin committed Apr 21, 2023
1 parent 4038e6a commit 339ce82
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 78 deletions.
159 changes: 83 additions & 76 deletions src/react/backend/location/PauseAndResume.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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 = (
<Box>
{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.'}
</Box>
);

if (replication === 'enabled' || ingestion === 'enabled') {
if (replicationStatus === 'enabled' || ingestionStatus === 'enabled') {
return (
<Box display="flex" alignItems="center">
<InlineButton
disabled={loading}
icon={<Icon name="Pause-circle" />}
disabled={isLoadingButton}
icon={isLoadingButton ? <Loader /> : <Icon name="Pause-circle" />}
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"
Expand All @@ -137,25 +139,30 @@ export const PauseAndResume = ({ locationName }: { locationName: string }) => {
);
}

if (replication === 'disabled' || ingestion === 'disabled') {
if (replicationStatus === 'disabled' || ingestionStatus === 'disabled') {
return (
<Box display="flex" alignItems="center">
<InlineButton
disabled={loading}
icon={<Icon name="Play-circle" />}
disabled={isLoadingButton}
icon={isLoadingButton ? <Loader /> : <Icon name="Play-circle" />}
tooltip={{
overlay: tooltip,
placement: 'top',
}}
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"
/>
Expand Down
102 changes: 100 additions & 2 deletions src/react/backend/location/__tests__/PauseAndResume.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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' });
Expand Down Expand Up @@ -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(<PauseAndResume locationName={locationName} />, {
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(
Expand Down

0 comments on commit 339ce82

Please sign in to comment.