mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
fe6060d22a
commit
bbca768e04
4 changed files with 138 additions and 32 deletions
|
@ -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'}
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -554,6 +554,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
<AgentActivityFlyout
|
||||
onAbortSuccess={fetchData}
|
||||
onClose={() => setAgentActivityFlyoutOpen(false)}
|
||||
refreshAgentActivity={isLoading}
|
||||
/>
|
||||
</EuiPortal>
|
||||
) : null}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue