Skip to content
Merged
89 changes: 89 additions & 0 deletions dashboard/src/hooks/__tests__/useVirtualization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
Comment thread
Brijesh619 marked this conversation as resolved.
Comment thread
Brijesh619 marked this conversation as resolved.
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { renderHook } from '@testing-library/react';
import { useVirtualization } from '../useVirtualization';

describe('useVirtualization', () => {
it('should return empty values when items array is empty', () => {
const { result } = renderHook(() =>
useVirtualization({ items: [], scrollTop: 0 })
);

expect(result.current).toEqual({
visibleItems: [],
paddingTop: 0,
paddingBottom: 0,
startIndex: 0,
});
});

it('should calculate visible items and padding correctly for initial state', () => {
const items = Array.from({ length: 100 }, (_, i) => i);
const { result } = renderHook(() =>
useVirtualization({ items, scrollTop: 0, itemHeight: 37, overscan: 10, visibleCount: 40 })
);

// Initial state (scrollTop = 0)
// startIndex = max(0, 0 - 10) = 0
// endIndex = min(99, 0 + 40 + 10) = 50
// visibleItems = items.slice(0, 51)

expect(result.current.startIndex).toBe(0);
expect(result.current.visibleItems.length).toBe(51);
expect(result.current.paddingTop).toBe(0);

// paddingBottom = (100 - 1 - 50) * 37 = 49 * 37 = 1813
expect(result.current.paddingBottom).toBe(1813);
});

it('should calculate visible items correctly when scrolled down', () => {
const items = Array.from({ length: 100 }, (_, i) => i);
// Scrolled 20 items down: 20 * 37 = 740
const { result } = renderHook(() =>
useVirtualization({ items, scrollTop: 740, itemHeight: 37, overscan: 10, visibleCount: 40 })
);

// startIndex = max(0, 20 - 10) = 10
// endIndex = min(99, 20 + 40 + 10) = 70

expect(result.current.startIndex).toBe(10);
expect(result.current.visibleItems.length).toBe(61); // 70 - 10 + 1

// paddingTop = 10 * 37 = 370
expect(result.current.paddingTop).toBe(370);

// paddingBottom = (100 - 1 - 70) * 37 = 29 * 37 = 1073
expect(result.current.paddingBottom).toBe(1073);
});

it('should cap end index at total items length', () => {
const items = Array.from({ length: 50 }, (_, i) => i);
// Scrolled way past the bottom
const { result } = renderHook(() =>
useVirtualization({ items, scrollTop: 5000, itemHeight: 37, overscan: 10, visibleCount: 40 })
);

// startIndex = max(0, 135 - 10) = 125
// endIndex = min(49, 135 + 40 + 10) = 49
// Wait, if startIndex > endIndex, slice will return empty array

expect(result.current.startIndex).toBe(125);
expect(result.current.visibleItems.length).toBe(0);
expect(result.current.paddingBottom).toBe(0);
});
});
33 changes: 33 additions & 0 deletions dashboard/src/hooks/useVirtualization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { useMemo } from 'react';
import { virtualizeList } from '../utils/Utils';

interface UseVirtualizationProps<T> {
items: T[];
scrollTop: number;
itemHeight?: number;
overscan?: number;
visibleCount?: number;
}

export const useVirtualization = <T>(options: UseVirtualizationProps<T>) => {
return useMemo(() => {
return virtualizeList(options);
}, [options.items, options.scrollTop, options.itemHeight, options.overscan, options.visibleCount]);
};
14 changes: 14 additions & 0 deletions dashboard/src/utils/Enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ export const auditAction: { [key: string]: string } = {
AUTO_PURGE : "Auto Purged Entities"
};


export enum AuditOperation {
PURGE = "PURGE",
AUTO_PURGE = "AUTO_PURGE",
IMPORT = "IMPORT",
EXPORT = "EXPORT"
}

export enum PurgeActiveView {
NONE = "none",
REQUESTED = "requested",
PURGED = "purged"
}

export const stats: any = {
generalData: {
collectionTime: "day"
Expand Down
43 changes: 42 additions & 1 deletion dashboard/src/utils/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,45 @@ const getNavigate = () => {
return backNavigate;
};

const virtualizeList = (options: {
items: any[];
scrollTop: number;
itemHeight?: number;
overscan?: number;
visibleCount?: number;
}) => {
const items = options.items || [];
const scrollTop = options.scrollTop || 0;
const itemHeight = options.itemHeight || 37;
const overscan = options.overscan || 10;
const visibleCount = options.visibleCount || 40;

const totalItems = items.length;
if (totalItems === 0) {
return {
visibleItems: [],
paddingTop: 0,
paddingBottom: 0,
startIndex: 0,
};
}

const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(totalItems - 1, Math.floor(scrollTop / itemHeight) + visibleCount + overscan);

const visibleItems = items.slice(startIndex, endIndex + 1);

const paddingTop = startIndex * itemHeight;
const paddingBottom = Math.max(0, (totalItems - 1 - endIndex) * itemHeight);

return {
visibleItems,
paddingTop,
paddingBottom,
startIndex,
};
};

const globalSearchFilterInitialQuery: any = {
query: {},
setQuery: (newQuery: any) => {
Expand Down Expand Up @@ -857,5 +896,7 @@ export {
setNavigate,
getNavigate,
globalSearchParams,
globalSearchFilterInitialQuery
globalSearchFilterInitialQuery,
virtualizeList
};

35 changes: 30 additions & 5 deletions dashboard/src/views/Administrator/Audits/AdminAuditTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,33 @@ const AdminAuditTable = () => {
const limit = pageSize || 25;
const offset = (pageIndex || 0) * limit;

let params: any = {
auditFilters: !isEmpty(queryApiObj) ? queryApiObj : null,
let auditFilters = !isEmpty(queryApiObj) ? JSON.parse(JSON.stringify(queryApiObj)) : null;

if (auditFilters) {
const filtersStr = JSON.stringify(auditFilters);
if (filtersStr.includes('"attributeName":"runId"')) {
// Remove any existing auditRowKind to prevent duplicates/conflicts
const removeAuditRowKind = (node: Record<string, any>) => {
if (node && node.criterion) {
node.criterion = node.criterion.filter((c: Record<string, any>) => c.attributeName !== 'auditRowKind');
node.criterion.forEach(removeAuditRowKind);
}
};
removeAuditRowKind(auditFilters);

// Force append SUMMARY by wrapping the existing filter
auditFilters = {
condition: "AND",
criterion: [
auditFilters,
{ attributeName: "auditRowKind", operator: "eq", attributeValue: "SUMMARY" }
]
};
}
}

let params: Record<string, unknown> = {
auditFilters: auditFilters,
limit: limit,
sortOrder: "DESCENDING",
offset: offset,
Expand All @@ -65,10 +90,10 @@ const AdminAuditTable = () => {
try {
setLoader(true);
let searchResp = await getAuditData(params);
setAuditData(searchResp.data);
setAuditData(searchResp.data || []);
setLoader(false);
} catch (error: any) {
console.error("Error fetching data:", error.response.data.errorMessage);
} catch (error: unknown) {
console.error("Error fetching data:", (error as any)?.response?.data?.errorMessage || (error as any)?.message);
toast.dismiss(toastId.current);
serverError(error, toastId);
setLoader(false);
Expand Down
Loading
Loading