Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4b5a254
upcoming: [DPS-41595] - Initial changes for virtualisation
venkymano-akamai Apr 1, 2026
c2b899d
Merge branch 'aclp_develop' of github.com:ACLPManager/manager into au…
venkymano-akamai Apr 6, 2026
782ddd2
upcoming: [DPS-41595] - dependency updates
venkymano-akamai Apr 6, 2026
b798128
upcoming: [DPS-41595] - Loading indicator changes
venkymano-akamai Apr 6, 2026
30c95c0
upcoming: [DPS-41595] - remove / fix eslint warnings
venkymano-akamai Apr 6, 2026
2963d05
upcoming: [DPS-41595] - Add useHook to achieve the delay indicator logic
venkymano-akamai Apr 7, 2026
836db09
upcoming: [DPS-41595] - Modify useHook efficiently
venkymano-akamai Apr 7, 2026
caa5723
upcoming: [DPS-41595] - Remove unused styles
venkymano-akamai Apr 7, 2026
9ca445e
Merge branch 'aclp_develop' into autocomplete_virtualization
agorthi-akamai Apr 7, 2026
e356b2f
upcoming: [DPS-41595] - Remove unused styles
venkymano-akamai Apr 7, 2026
75d004a
upcoming: [DPS-41595] - Fix styles and do virtualization only if elem…
venkymano-akamai Apr 7, 2026
985cde7
Merge branch 'autocomplete_virtualization' of github.com:venkymano-ak…
venkymano-akamai Apr 7, 2026
b0f4343
upcoming: [DPS-41595] - Message modification
venkymano-akamai Apr 9, 2026
4af3ee7
upcoming: [DPS-41595] - Added a property descritption
venkymano-akamai Apr 9, 2026
074b619
upcoming: [DPS-41595] - Production grade code refactoring
venkymano-akamai Apr 9, 2026
de7f5a1
upcoming: [DPS-41595] - Lint issue fixes
venkymano-akamai Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"react-redux": "~7.1.3",
"react-vnc": "^3.0.7",
"react-waypoint": "^10.3.0",
"react-window": "^1.8.10",
"recharts": "^2.14.1",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0",
Expand Down Expand Up @@ -150,6 +151,7 @@
"@types/react-csv": "^1.1.3",
"@types/react-dom": "^19.1.6",
"@types/react-redux": "~7.1.7",
"@types/react-window": "^1.8.5",
"@types/redux-mock-store": "^1.0.1",
"@types/throttle-debounce": "^1.0.0",
"@types/zxcvbn": "^4.4.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import EntityIcon from 'src/assets/icons/entityIcons/alertsresources.svg';
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
import { useResourcesQuery } from 'src/queries/cloudpulse/resources';

import { DelayedLoadingMessage } from '../../shared/DelayedLoadingMessage';
import { LOADING_DELAYS } from '../../Utils/constants';
import { useDelayedLoadingIndicator } from '../../Utils/useDelayedLoadingIndicator';
import { StyledPlaceholder } from '../AlertsDetail/AlertDetail';
import { MULTILINE_ERROR_SEPARATOR } from '../constants';
import { AlertListNoticeMessages } from '../Utils/AlertListNoticeMessages';
Expand Down Expand Up @@ -370,6 +373,14 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => {
!isDataLoadingError && !isSelectionsNeeded && alertResourceIds.length === 0;
const showEditInformation = isSelectionsNeeded && alertType === 'system';

const isLoading = isRegionsLoading || isResourcesLoading;

// Show loading indicator only if loading continues for more than 10 seconds
const showLoadingIndicator = useDelayedLoadingIndicator(
isLoading,
LOADING_DELAYS.LARGE_DATASET
);

if (isNoResources) {
return (
<Stack gap={2}>
Expand Down Expand Up @@ -407,10 +418,14 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => {
maxSelectionCount && selectedResources
? Math.max(0, maxSelectionCount - selectedResources.length)
: undefined;
const isLoading = isRegionsLoading || isResourcesLoading;
return (
<Stack gap={2}>
{isLoading && <CircleProgress />}
{isLoading && (
<Stack alignItems="center" gap={2}>
<CircleProgress />
{showLoadingIndicator && <DelayedLoadingMessage />}
</Stack>
)}
{!hideLabel && (
<Typography
display={isLoading ? 'none' : 'block'}
Expand Down
24 changes: 24 additions & 0 deletions packages/manager/src/features/CloudPulse/Utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,27 @@ export const ASSOCIATED_ENTITY_METRIC_MAP: Record<
linode: 'Linode',
nodebalancer: 'Node Balancer',
};

/**
* Configuration constants for virtualized list rendering
*/
export const VIRTUALIZATION_CONFIG = {
/** Height of each item in the virtualized list (px) */
ITEM_HEIGHT: 36,
/** Maximum visible height of the virtualized list (px) - shows ~8 items */
MAX_VISIBLE_HEIGHT: 288,
/** Minimum number of items before virtualization is enabled */
THRESHOLD: 100,
/** Maximum number of filtered results to show when searching */
FILTER_LIMIT: 1300,
};

/**
* Delay times for loading indicators (milliseconds)
*/
export const LOADING_DELAYS = {
/** Default delay before showing loading indicator */
DEFAULT: 5000,
/** Extended delay for large dataset operations */
LARGE_DATASET: 10000,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { useDelayedLoadingIndicator } from './useDelayedLoadingIndicator';

describe('useDelayedLoadingIndicator', () => {
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});

it('should return false initially when not loading', () => {
const { result } = renderHook(() => useDelayedLoadingIndicator(false));

expect(result.current).toBe(false);
});

it('should return false initially even when loading starts', () => {
const { result } = renderHook(() => useDelayedLoadingIndicator(true));

expect(result.current).toBe(false);
});

it('should return true after default delay (5000ms) when loading', () => {
const { result } = renderHook(() => useDelayedLoadingIndicator(true));

expect(result.current).toBe(false);

act(() => {
vi.advanceTimersByTime(4999);
});

expect(result.current).toBe(false);

act(() => {
vi.advanceTimersByTime(1);
});

expect(result.current).toBe(true);
});

it('should return true after custom delay when provided', () => {
const customDelay = 3000;
const { result } = renderHook(() =>
useDelayedLoadingIndicator(true, customDelay)
);

expect(result.current).toBe(false);

act(() => {
vi.advanceTimersByTime(2999);
});

expect(result.current).toBe(false);

act(() => {
vi.advanceTimersByTime(1);
});

expect(result.current).toBe(true);
});

it('should reset to false immediately when loading completes before delay', () => {
const { result, rerender } = renderHook(
({ isLoading }) => useDelayedLoadingIndicator(isLoading),
{ initialProps: { isLoading: true } }
);

expect(result.current).toBe(false);

act(() => {
vi.advanceTimersByTime(3000);
});

expect(result.current).toBe(false);

// Loading completes before delay
rerender({ isLoading: false });

expect(result.current).toBe(false);
});

it('should reset to false immediately when loading completes after delay', () => {
const { result, rerender } = renderHook(
({ isLoading }) => useDelayedLoadingIndicator(isLoading),
{ initialProps: { isLoading: true } }
);

// Wait for delay to pass
act(() => {
vi.advanceTimersByTime(5000);
});

expect(result.current).toBe(true);

// Loading completes
rerender({ isLoading: false });

expect(result.current).toBe(false);
});

it('should handle rapid loading state changes correctly', () => {
const { result, rerender } = renderHook(
({ isLoading }) => useDelayedLoadingIndicator(isLoading),
{ initialProps: { isLoading: true } }
);

expect(result.current).toBe(false);

// Stop loading before delay
act(() => {
vi.advanceTimersByTime(2000);
});
rerender({ isLoading: false });

expect(result.current).toBe(false);

// Start loading again
rerender({ isLoading: true });

expect(result.current).toBe(false);

// Wait for new delay
act(() => {
vi.advanceTimersByTime(5000);
});

expect(result.current).toBe(true);
});

it('should clear timeout on unmount', () => {
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');

const { unmount } = renderHook(() => useDelayedLoadingIndicator(true));

unmount();

expect(clearTimeoutSpy).toHaveBeenCalled();
});

it('should clear timeout when isLoading changes from true to false', () => {
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');

const { rerender } = renderHook(
({ isLoading }) => useDelayedLoadingIndicator(isLoading),
{ initialProps: { isLoading: true } }
);

rerender({ isLoading: false });

expect(clearTimeoutSpy).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useEffect, useState } from 'react';

/**
* Custom hook to show a loading indicator only after a specified delay.
* Useful for preventing loading flashes for quick operations while still
* providing feedback for longer-running tasks.
*
* @param isLoading - The loading state to monitor
* @param delay - Delay in milliseconds before showing the indicator (default: 5000)
* @returns boolean indicating whether to show the delayed loading indicator
*
* @example
* ```tsx
* const showLoadingIndicator = useDelayedLoadingIndicator(isLoading);
*
* return (
* <>
* {isLoading && <CircleProgress />}
* {showLoadingIndicator && (
* <Typography>Taking longer than expected...</Typography>
* )}
* </>
* );
* ```
*/
export const useDelayedLoadingIndicator = (
isLoading: boolean,
delay: number = 5000
): boolean => {
const [showLoadingIndicator, setShowLoadingIndicator] =
useState<boolean>(false);

useEffect(() => {
if (!isLoading) {
// Reset the indicator immediately when loading completes
setShowLoadingIndicator(false);
return;
}

// Set a timer to show loading indicator after the specified delay
const timer = setTimeout(() => {
setShowLoadingIndicator(true);
}, delay);

// Clean up timer on unmount or when dependencies change
return () => {
clearTimeout(timer);
};
}, [isLoading, delay]);

return showLoadingIndicator;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ENDPOINT,
FIREWALL,
INTERFACE_ID,
LOADING_DELAYS,
NODE_TYPE,
NODEBALANCER_ID,
PARENT_ENTITY_REGION,
Expand All @@ -36,7 +37,9 @@ import {
} from '../Utils/FilterBuilder';
import { FILTER_CONFIG } from '../Utils/FilterConfig';
import { type CloudPulseServiceTypeFilters } from '../Utils/models';
import { useDelayedLoadingIndicator } from '../Utils/useDelayedLoadingIndicator';
import { clearChildPreferences } from '../Utils/UserPreference';
import { DelayedLoadingMessage } from './DelayedLoadingMessage';

import type {
CloudPulseMetricsFilter,
Expand Down Expand Up @@ -115,6 +118,12 @@ export const CloudPulseDashboardFilterBuilder = React.memo(
const dependentFilterReference: React.MutableRefObject<CloudPulseMetricsFilter> =
React.useRef({});

// Show loading indicator only if loading continues for more than 10 seconds
const showLoadingIndicator = useDelayedLoadingIndicator(
isLoading,
LOADING_DELAYS.LARGE_DATASET
);

const checkAndUpdateDependentFilters = React.useCallback(
(filterKey: string, value: FilterValueType) => {
if (dashboard && dashboard.service_type) {
Expand Down Expand Up @@ -549,10 +558,12 @@ export const CloudPulseDashboardFilterBuilder = React.memo(
<GridLegacy
alignItems="center"
container
direction="column"
display="flex"
justifyContent="center"
>
<CircleProgress size="md" />
{showLoadingIndicator && <DelayedLoadingMessage />}
</GridLegacy>
) : (
<GridLegacy
Expand Down
Loading
Loading