[Fleet] reloading activity in sync with agents, added loading spinner (#140958)

* reloading activity in sync with agents, added loading spinner

* renamed variable
This commit is contained in:
Julia Bardi 2022-09-20 08:52:55 +02:00 committed by GitHub
parent fe6060d22a
commit bbca768e04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 138 additions and 32 deletions

View file

@ -32,6 +32,8 @@ import { useActionStatus } from '../hooks';
import { useGetAgentPolicies, useStartServices } from '../../../../hooks';
import { SO_SEARCH_LIMIT } from '../../../../constants';
import { Loading } from '../../components';
import { getTodayActions, getOtherDaysActions } from './agent_activity_helper';
const FullHeightFlyoutBody = styled(EuiFlyoutBody)`
@ -47,12 +49,16 @@ const FlyoutFooterWPadding = styled(EuiFlyoutFooter)`
export const AgentActivityFlyout: React.FunctionComponent<{
onClose: () => void;
onAbortSuccess: () => void;
}> = ({ onClose, onAbortSuccess }) => {
refreshAgentActivity: boolean;
}> = ({ onClose, onAbortSuccess, refreshAgentActivity }) => {
const { data: agentPoliciesData } = useGetAgentPolicies({
perPage: SO_SEARCH_LIMIT,
});
const { currentActions, abortUpgrade } = useActionStatus(onAbortSuccess);
const { currentActions, abortUpgrade, isFirstLoading } = useActionStatus(
onAbortSuccess,
refreshAgentActivity
);
const getAgentPolicyName = (policyId: string) => {
const policy = agentPoliciesData?.items.find((item) => item.id === policyId);
@ -102,7 +108,18 @@ export const AgentActivityFlyout: React.FunctionComponent<{
</EuiFlyoutHeader>
<FullHeightFlyoutBody>
{currentActionsEnriched.length === 0 ? (
{isFirstLoading ? (
<EuiFlexGroup
direction="row"
justifyContent={'center'}
alignItems={'center'}
className="eui-fullHeight"
>
<EuiFlexItem>
<Loading />
</EuiFlexItem>
</EuiFlexGroup>
) : currentActionsEnriched.length === 0 ? (
<EuiFlexGroup
direction="column"
justifyContent={'center'}

View file

@ -0,0 +1,105 @@
/*
* 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 { renderHook, act } from '@testing-library/react-hooks';
import { sendGetActionStatus, sendPostCancelAction, useStartServices } from '../../../../hooks';
import { useActionStatus } from './use_action_status';
jest.mock('../../../../hooks', () => ({
sendGetActionStatus: jest.fn(),
sendPostCancelAction: jest.fn(),
useStartServices: jest.fn().mockReturnValue({
notifications: {
toasts: {
addError: jest.fn(),
},
},
overlays: {
openConfirm: jest.fn(),
},
}),
}));
describe('useActionStatus', () => {
const mockSendGetActionStatus = sendGetActionStatus as jest.Mock;
const mockSendPostCancelAction = sendPostCancelAction as jest.Mock;
const startServices = useStartServices();
const mockOpenConfirm = startServices.overlays.openConfirm as jest.Mock;
const mockErrorToast = startServices.notifications.toasts.addError as jest.Mock;
const mockOnAbortSuccess = jest.fn();
const mockActionStatuses = [
{
actionId: 'action1',
nbAgentsActionCreated: 2,
nbAgentsAck: 1,
nbAgentsFailed: 0,
nbAgentsActioned: 2,
creationTime: '2022-09-19T12:07:27.102Z',
},
];
beforeEach(() => {
mockSendGetActionStatus.mockReset();
mockSendGetActionStatus.mockResolvedValue({ data: { items: mockActionStatuses } });
mockSendPostCancelAction.mockReset();
mockOnAbortSuccess.mockReset();
mockOpenConfirm.mockReset();
mockOpenConfirm.mockResolvedValue({});
mockErrorToast.mockReset();
mockErrorToast.mockResolvedValue({});
});
it('should set action statuses on init', async () => {
let result: any | undefined;
await act(async () => {
({ result } = renderHook(() => useActionStatus(mockOnAbortSuccess, false)));
});
expect(result?.current.currentActions).toEqual(mockActionStatuses);
});
it('should refresh statuses on refresh flag', async () => {
let refresh = false;
await act(async () => {
const result = renderHook(() => useActionStatus(mockOnAbortSuccess, refresh));
refresh = true;
result.rerender();
});
expect(mockSendGetActionStatus).toHaveBeenCalledTimes(2);
});
it('should post abort and invoke callback on abort upgrade', async () => {
mockSendPostCancelAction.mockResolvedValue({});
let result: any | undefined;
await act(async () => {
({ result } = renderHook(() => useActionStatus(mockOnAbortSuccess, false)));
});
await act(async () => {
await result.current.abortUpgrade(mockActionStatuses[0]);
});
expect(mockSendPostCancelAction).toHaveBeenCalledWith('action1');
expect(mockOnAbortSuccess).toHaveBeenCalled();
expect(mockOpenConfirm).toHaveBeenCalledWith('This action will abort upgrade of 1 agents', {
title: 'Abort upgrade?',
});
});
it('should report error on abort upgrade failure', async () => {
const error = new Error('error');
mockSendPostCancelAction.mockRejectedValue(error);
let result: any | undefined;
await act(async () => {
({ result } = renderHook(() => useActionStatus(mockOnAbortSuccess, false)));
});
await act(async () => {
await result.current.abortUpgrade(mockActionStatuses[0]);
});
expect(mockOnAbortSuccess).not.toHaveBeenCalled();
expect(mockErrorToast).toHaveBeenCalledWith(error, {
title: 'An error happened while aborting upgrade',
});
});
});

View file

@ -5,27 +5,22 @@
* 2.0.
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { sendGetActionStatus, sendPostCancelAction, useStartServices } from '../../../../hooks';
import type { ActionStatus } from '../../../../types';
const POLL_INTERVAL = 30 * 1000;
export function useActionStatus(onAbortSuccess: () => void) {
export function useActionStatus(onAbortSuccess: () => void, refreshAgentActivity: boolean) {
const [currentActions, setCurrentActions] = useState<ActionStatus[]>([]);
const currentTimeoutRef = useRef<NodeJS.Timeout>();
const isCancelledRef = useRef<boolean>(false);
const [isFirstLoading, setIsFirstLoading] = useState(true);
const { notifications, overlays } = useStartServices();
const refreshActions = useCallback(async () => {
try {
const res = await sendGetActionStatus();
if (isCancelledRef.current) {
return;
}
setIsFirstLoading(false);
if (res.error) {
throw res.error;
}
@ -44,28 +39,15 @@ export function useActionStatus(onAbortSuccess: () => void) {
}
}, [notifications.toasts]);
// Poll for upgrades
if (isFirstLoading) {
refreshActions();
}
useEffect(() => {
isCancelledRef.current = false;
async function pollData() {
await refreshActions();
if (isCancelledRef.current) {
return;
}
currentTimeoutRef.current = setTimeout(() => pollData(), POLL_INTERVAL);
if (refreshAgentActivity) {
refreshActions();
}
pollData();
return () => {
isCancelledRef.current = true;
if (currentTimeoutRef.current) {
clearTimeout(currentTimeoutRef.current);
}
};
}, [refreshActions]);
}, [refreshActions, refreshAgentActivity]);
const abortUpgrade = useCallback(
async (action: ActionStatus) => {
@ -104,5 +86,6 @@ export function useActionStatus(onAbortSuccess: () => void) {
currentActions,
refreshActions,
abortUpgrade,
isFirstLoading,
};
}

View file

@ -554,6 +554,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
<AgentActivityFlyout
onAbortSuccess={fetchData}
onClose={() => setAgentActivityFlyoutOpen(false)}
refreshAgentActivity={isLoading}
/>
</EuiPortal>
) : null}