diff --git a/superset-frontend/packages/superset-ui-core/src/utils/lruCache.ts b/superset-frontend/packages/superset-ui-core/src/utils/lruCache.ts index f6785850c22a..e92005986aa3 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/lruCache.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/lruCache.ts @@ -67,6 +67,10 @@ class LRUCache { public get size() { return this.cache.size; } + + public values(): T[] { + return [...this.cache.values()]; + } } export function lruCache(capacity = 100) { diff --git a/superset-frontend/packages/superset-ui-core/test/utils/lruCache.test.ts b/superset-frontend/packages/superset-ui-core/test/utils/lruCache.test.ts index f8a077eba031..2c7f1fafa404 100644 --- a/superset-frontend/packages/superset-ui-core/test/utils/lruCache.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/utils/lruCache.test.ts @@ -35,8 +35,11 @@ test('LRU operations', () => { expect(cache.size).toBe(3); expect(cache.has('1')).toBeFalsy(); expect(cache.get('1')).toBeUndefined(); + expect(cache.values()).toEqual(['b', 'c', 'd']); cache.get('2'); + expect(cache.values()).toEqual(['c', 'd', 'b']); cache.set('5', 'e'); + expect(cache.values()).toEqual(['d', 'b', 'e']); expect(cache.has('2')).toBeTruthy(); expect(cache.has('3')).toBeFalsy(); // @ts-expect-error @@ -44,6 +47,7 @@ test('LRU operations', () => { // @ts-expect-error expect(() => cache.get(0)).toThrow(TypeError); expect(cache.size).toBe(3); + expect(cache.values()).toEqual(['d', 'b', 'e']); cache.clear(); expect(cache.size).toBe(0); expect(cache.capacity).toBe(3); diff --git a/superset-frontend/src/features/home/ActivityTable.tsx b/superset-frontend/src/features/home/ActivityTable.tsx index dc456015ff73..3eb35598ac2a 100644 --- a/superset-frontend/src/features/home/ActivityTable.tsx +++ b/superset-frontend/src/features/home/ActivityTable.tsx @@ -33,19 +33,7 @@ import { Chart } from 'src/types/Chart'; import Icons from 'src/components/Icons'; import SubMenu from './SubMenu'; import EmptyState from './EmptyState'; -import { WelcomeTable } from './types'; - -/** - * Return result from /api/v1/log/recent_activity/ - */ -interface RecentActivity { - action: string; - item_type: 'slice' | 'dashboard'; - item_url: string; - item_title: string; - time: number; - time_delta_humanized?: string; -} +import { WelcomeTable, RecentActivity } from './types'; interface RecentSlice extends RecentActivity { item_type: 'slice'; diff --git a/superset-frontend/src/features/home/types.ts b/superset-frontend/src/features/home/types.ts index 105cded78f72..a59e9fcd8586 100644 --- a/superset-frontend/src/features/home/types.ts +++ b/superset-frontend/src/features/home/types.ts @@ -55,3 +55,15 @@ export enum GlobalMenuDataOptions { ExcelUpload = 'excelUpload', ColumnarUpload = 'columnarUpload', } + +/** + * Return result from /api/v1/log/recent_activity/ + */ +export interface RecentActivity { + action: string; + item_type: 'slice' | 'dashboard'; + item_url: string; + item_title: string; + time: number; + time_delta_humanized?: string; +} diff --git a/superset-frontend/src/pages/Home/Home.test.tsx b/superset-frontend/src/pages/Home/Home.test.tsx index ea0bc161cbcd..e9dd039cbdb8 100644 --- a/superset-frontend/src/pages/Home/Home.test.tsx +++ b/superset-frontend/src/pages/Home/Home.test.tsx @@ -65,9 +65,35 @@ fetchMock.get(savedQueryEndpoint, { result: [], }); +const mockRecentActivityResult = [ + { + action: 'dashboard', + item_title: "World Bank's Data", + item_type: 'dashboard', + item_url: '/superset/dashboard/world_health/', + time: 1741644942130.566, + time_delta_humanized: 'a day ago', + }, + { + action: 'dashboard', + item_title: '[ untitled dashboard ]', + item_type: 'dashboard', + item_url: '/superset/dashboard/19/', + time: 1741644881695.7869, + time_delta_humanized: 'a day ago', + }, + { + action: 'dashboard', + item_title: '[ untitled dashboard ]', + item_type: 'dashboard', + item_url: '/superset/dashboard/19/', + time: 1741644381695.7869, + time_delta_humanized: 'two day ago', + }, +]; + fetchMock.get(recentActivityEndpoint, { - Created: [], - Viewed: [], + result: mockRecentActivityResult, }); fetchMock.get(chartInfoEndpoint, { @@ -149,6 +175,20 @@ test('With sql role - renders all panels on the page on page load', async () => expect(panels).toHaveLength(4); }); +test('With sql role - renders distinct recent activities', async () => { + await renderWelcome(); + const recentPanel = screen.getByRole('button', { name: 'right Recents' }); + userEvent.click(recentPanel); + await waitFor(() => + expect( + screen.queryAllByText(mockRecentActivityResult[0].item_title), + ).toHaveLength(1), + ); + expect( + screen.queryAllByText(mockRecentActivityResult[1].item_title), + ).toHaveLength(1); +}); + test('With sql role - calls api methods in parallel on page load', async () => { await renderWelcome(); expect(fetchMock.calls(chartsEndpoint)).toHaveLength(2); diff --git a/superset-frontend/src/pages/Home/index.tsx b/superset-frontend/src/pages/Home/index.tsx index 90466fe96e7c..6d005c79f6f6 100644 --- a/superset-frontend/src/pages/Home/index.tsx +++ b/superset-frontend/src/pages/Home/index.tsx @@ -156,7 +156,7 @@ function Welcome({ user, addDangerToast }: WelcomeProps) { const canReadSavedQueries = userHasPermission(user, 'SavedQuery', 'can_read'); const userid = user.userId; const id = userid!.toString(); // confident that user is not a guest user - const params = rison.encode({ page_size: 6 }); + const params = rison.encode({ page_size: 24, distinct: false }); const recent = `/api/v1/log/recent_activity/?q=${params}`; const [activeChild, setActiveChild] = useState('Loading'); const userKey = dangerouslyGetItemDoNotUse(id, null); diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 8fd77373b532..86075e30a910 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -26,6 +26,7 @@ import { SupersetTheme, getClientErrorObject, t, + lruCache, } from '@superset-ui/core'; import Chart from 'src/types/Chart'; import { intersection } from 'lodash'; @@ -34,7 +35,7 @@ import { FetchDataConfig, FilterValue } from 'src/components/ListView'; import SupersetText from 'src/utils/textUtils'; import { findPermission } from 'src/utils/findPermission'; import { User } from 'src/types/bootstrapTypes'; -import { WelcomeTable } from 'src/features/home/types'; +import { RecentActivity, WelcomeTable } from 'src/features/home/types'; import { Dashboard, Filter, TableTab } from './types'; // Modifies the rison encoding slightly to match the backend's rison encoding/decoding. Applies globally. @@ -223,10 +224,14 @@ export const getRecentActivityObjs = ( ) => SupersetClient.get({ endpoint: recent }).then(recentsRes => { const res: any = {}; + const distinctRes = lruCache(6); + recentsRes.json.result.reverse().forEach((record: RecentActivity) => { + distinctRes.set(record.item_url, record); + }); return getFilteredChartsandDashboards(addDangerToast, filters).then( ({ other }) => { res.other = other; - res.viewed = recentsRes.json.result; + res.viewed = distinctRes.values().reverse(); return res; }, );