Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 22 additions & 19 deletions galasa-ui/src/components/test-runs/results/TestRunsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ export default function TestRunsTable({

const currentVisibleColumns = visibleColumns.join(',');

// Memoize formatted dates for all runs to avoid repeated formatting
const formattedDatesCache = useMemo(() => {
const cache = new Map<string, string>();
if (runsList && runsList.length > 0) {
runsList.forEach((row) => {
if (row.submittedAt && !cache.has(row.submittedAt)) {
cache.set(row.submittedAt, formatDate(new Date(row.submittedAt)));
}
});
}
return cache;
}, [runsList, formatDate]);

// Filter rows in the current paginatedRows if the text in the
// persistent toolbar search box changes and matches any table info.
const filteredRows = useMemo(() => {
Expand All @@ -110,22 +123,18 @@ export default function TestRunsTable({
return false;
}
let value = '';
// Reimplement in future - formatDate causes performance issues...
// if (field === 'submittedAt') {
// value =
// row.submittedAt?.trim() !== ''
// ? formatDate(new Date(row.submittedAt.toLowerCase()))
// : '';
// }
if (field === 'tags') {
if (field === 'submittedAt') {
value =
row.submittedAt?.trim() !== '' ? formattedDatesCache.get(row.submittedAt) || '' : '';
} else if (field === 'tags') {
value = row.tags?.trim() !== '' ? row.tags.toLowerCase() : 'n/a';
} else {
value = row[field]?.toLowerCase() ?? '';
}
return value.includes(searchLowerCase);
});
});
}, [currentVisibleColumns, runsList, search]);
}, [currentVisibleColumns, runsList, search, formattedDatesCache]);

// Calculate the paginated rows based on the current page and page size,
// and currently filtered rows (if there is a filter in the toolbar)
Expand Down Expand Up @@ -219,7 +228,7 @@ export default function TestRunsTable({
};

// Navigate to the test run details page using the runId
const handleRowClick = (runId: string, runName: string) => {
const handleRowClick = (runId: string) => {
// Navigate to the test run details page
router.push(`/test-runs/${runId}`);
};
Expand Down Expand Up @@ -262,10 +271,10 @@ export default function TestRunsTable({
</TableCell>
);
} else if (header === 'submittedAt') {
// Format the date using the context's formatDate function
const formattedDateValue = formattedDatesCache.get(value) || formatDate(new Date(value));
cellComponent = (
<TableCell className={styles.linkCell}>
{formatDate(new Date(value))}
{formattedDateValue}
<Link href={href} prefetch={false} className={styles.linkOverlay} />
</TableCell>
);
Expand Down Expand Up @@ -356,13 +365,7 @@ export default function TestRunsTable({
<TableRow
key={key}
{...rowProps}
onClick={() =>
handleRowClick(
row.id,
row.cells.find((cell) => cell.info.header === 'testRunName')
?.value as string
)
}
onClick={() => handleRowClick(row.id)}
id={styles.clickableRow}
>
{row.cells.map((cell) => (
Expand Down
47 changes: 31 additions & 16 deletions galasa-ui/src/contexts/DateTimeFormatContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
TimeZone,
TimeZoneFormats,
} from '@/utils/types/dateTimeSettings';
import { useCallback, useState, createContext, useContext } from 'react';
import { useCallback, useState, createContext, useContext, useRef } from 'react';

const LOCAL_STORAGE_KEY = 'dateTimeFormatSettings';

Expand Down Expand Up @@ -67,6 +67,9 @@ export function DateTimeFormatProvider({ children }: { children: React.ReactNode
return currentPreferences;
});

// Cache for Intl.DateTimeFormat instances to avoid expensive re-creation
const formattersCache = useRef<Map<string, Intl.DateTimeFormat>>(new Map());

const updatePreferences = (newPreferences: Partial<typeof preferences>) => {
const updatedPreferences = { ...preferences, ...newPreferences };

Expand All @@ -82,6 +85,9 @@ export function DateTimeFormatProvider({ children }: { children: React.ReactNode
updatedPreferences[PREFERENCE_KEYS.TIME_ZONE] = defaultPreferences[PREFERENCE_KEYS.TIME_ZONE];
}

// Clear formatters cache when preferences change
formattersCache.current.clear();

setPreferences(updatedPreferences);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(updatedPreferences));
};
Expand All @@ -97,6 +103,20 @@ export function DateTimeFormatProvider({ children }: { children: React.ReactNode
return specifiedTimeZone;
}, [preferences.timeZoneType, preferences.timeZone]);

// Helper function to get or create a cached formatter
const getOrCreateFormatter = useCallback(
(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat => {
const cacheKey = JSON.stringify({ locale, options });

if (!formattersCache.current.has(cacheKey)) {
formattersCache.current.set(cacheKey, new Intl.DateTimeFormat(locale, options));
}

return formattersCache.current.get(cacheKey)!;
},
[]
);

const formatDate = useCallback(
(date: Date): string => {
let formattedDate: string = '-';
Expand All @@ -120,24 +140,19 @@ export function DateTimeFormatProvider({ children }: { children: React.ReactNode
timeZone: resolvedTimeZone,
};

// Define options specifically to extract the timezone name
const timeZoneNameOptions: Intl.DateTimeFormatOptions = {
timeZone: resolvedTimeZone,
timeZoneName: 'short', // e.g., PST, EDT, EEST
};

// Determine the locale to use
const effectiveLocale = dateTimeFormatType === 'browser' ? undefined : locale;

// Format the two parts separately
const mainPart = new Intl.DateTimeFormat(effectiveLocale, dateTimeOptions).format(date);
const mainFormatter = getOrCreateFormatter(effectiveLocale, dateTimeOptions);
const mainPart = mainFormatter.format(date);

// Get the full string with timezone
const fullStringWithTz = new Intl.DateTimeFormat(effectiveLocale, {
...dateTimeOptions,
...timeZoneNameOptions,
}).format(date);
const timeZonePart = fullStringWithTz.split(' ').pop() || '';
const timeZoneOptions: Intl.DateTimeFormatOptions = {
timeZone: resolvedTimeZone,
timeZoneName: 'short',
};
const timeZoneFormatter = getOrCreateFormatter(effectiveLocale, timeZoneOptions);
const parts = timeZoneFormatter.formatToParts(date);
const timeZonePart = parts.find((part) => part.type === 'timeZoneName')?.value || '';

// Combine them into the desired final format (e.g., "MM/DD/YYYY, HH:mm:ss (GMT+X)")
formattedDate = `${mainPart} (${timeZonePart})`;
Expand All @@ -147,7 +162,7 @@ export function DateTimeFormatProvider({ children }: { children: React.ReactNode

return formattedDate;
},
[preferences, getResolvedTimeZone]
[preferences, getResolvedTimeZone, getOrCreateFormatter]
);

const value = { preferences, updatePreferences, formatDate, getResolvedTimeZone };
Expand Down
93 changes: 93 additions & 0 deletions galasa-ui/src/tests/contexts/DateTimeFormatContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,99 @@ describe('DateTimeFormatContext', () => {
'Formatted Date: 10/01/2023, 09:00:00 PM (GMT+9)'
);
});

test('clears formatter cache when preferences are updated', () => {
render(
<DateTimeFormatProvider>
<TestComponent date={mockDate} />
</DateTimeFormatProvider>
);

// Update preferences
const button = screen.getByRole('button', { name: /Update Locale/i });
fireEvent.click(button);

// The formatted date should be updated with new locale
const updatedFormatted = screen.getByText(/Formatted Date:/).textContent;
expect(updatedFormatted).toBeDefined();
});
});

describe('Performance: Formatter caching', () => {
test('reuses cached formatters for multiple calls with same parameters', () => {
const dateTimeFormatSpy = jest.spyOn(Intl, 'DateTimeFormat');

const MultipleCallsComponent = () => {
const { formatDate } = useDateTimeFormat();
const date1 = new Date('2023-10-01T12:00:00Z');
const date2 = new Date('2023-10-02T12:00:00Z');
const date3 = new Date('2023-10-03T12:00:00Z');

return (
<div>
<p>Date 1: {formatDate(date1)}</p>
<p>Date 2: {formatDate(date2)}</p>
<p>Date 3: {formatDate(date3)}</p>
</div>
);
};

render(
<DateTimeFormatProvider>
<MultipleCallsComponent />
</DateTimeFormatProvider>
);

const callCount = dateTimeFormatSpy.mock.calls.length;
expect(callCount).toBeLessThan(10);

dateTimeFormatSpy.mockRestore();
});

test('creates new formatters after cache is cleared', () => {
const dateTimeFormatSpy = jest.spyOn(Intl, 'DateTimeFormat');

render(
<DateTimeFormatProvider>
<TestComponent date={mockDate} />
</DateTimeFormatProvider>
);

const initialCallCount = dateTimeFormatSpy.mock.calls.length;

// Update preferences which should clear the cache
const button = screen.getByRole('button', { name: /Update Locale/i });
fireEvent.click(button);

// After clearing cache, new formatters should be created
expect(dateTimeFormatSpy.mock.calls.length).toBeGreaterThan(initialCallCount);

dateTimeFormatSpy.mockRestore();
});

test('formatToParts is used for efficient timezone extraction', () => {
const formatToPartsSpy = jest.fn(() => [{ type: 'timeZoneName', value: 'UTC' }]);

jest.spyOn(Intl, 'DateTimeFormat').mockImplementation(
() =>
({
format: jest.fn(() => '10/01/2023, 12:00:00 PM'),
formatToParts: formatToPartsSpy,
resolvedOptions: jest.fn(() => ({ timeZone: 'UTC' })),
}) as unknown as Intl.DateTimeFormat
);

render(
<DateTimeFormatProvider>
<TestComponent date={mockDate} />
</DateTimeFormatProvider>
);

// Verify formatToParts was called for timezone extraction
expect(formatToPartsSpy).toHaveBeenCalled();

jest.restoreAllMocks();
});
});
});
});
Loading