Skip to content

Commit 247960a

Browse files
committed
fix(events): switch to blocklist — hide other workspace events, show rest
Previous approach (allowlist) filtered events to only those matching the current workspace name or id-prefix, which discarded generic cluster events (Deployment, ReplicaSet controllers) not yet associated with a workspace-derived pod name. New approach: include every event EXCEPT those whose involvedObject.name starts with another DevWorkspace's id, or exactly equals another DevWorkspace's name. This shows all current-workspace events (DevWorkspace CR, Deployment, ReplicaSet, Pod) plus any cluster event that does not belong to a different workspace. selectEventsFromResourceVersion now reads selectAllWorkspaces directly so the component no longer needs to compute the exclusion list. Assisted-by: Claude Sonnet 4.6 Signed-off-by: Oleksii Orel <oorel@redhat.com>
1 parent 9469afb commit 247960a

2 files changed

Lines changed: 188 additions & 82 deletions

File tree

packages/dashboard-frontend/src/store/Events/__tests__/selectors.spec.ts

Lines changed: 144 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
* Red Hat, Inc. - initial API and implementation
1111
*/
1212

13+
import { WorkspaceAdapter } from '@/services/workspace-adapter';
1314
import { RootState } from '@/store';
15+
import { DevWorkspaceBuilder } from '@/store/__mocks__/devWorkspaceBuilder';
16+
import { MockStoreBuilder } from '@/store/__mocks__/mockStore';
1417
import {
1518
selectAllEvents,
1619
selectEventsError,
@@ -30,90 +33,174 @@ describe('Events Selectors', () => {
3033
error: 'Something went wrong',
3134
resourceVersion: '125',
3235
},
33-
} as RootState;
36+
} as unknown as RootState;
3437

3538
it('should select all events', () => {
3639
const result = selectAllEvents(mockState);
3740
expect(result).toEqual(mockState.events.events);
3841
});
3942

4043
it('should select events from a specific resource version', () => {
41-
const selectFromResourceVersion = selectEventsFromResourceVersion(mockState);
42-
const result = selectFromResourceVersion('124');
43-
expect(result).toEqual([
44+
// No workspaces in store — all events pass the blocklist
45+
const store = new MockStoreBuilder()
46+
.withEvents({ events: mockState.events.events as never[], resourceVersion: '125' })
47+
.build();
48+
const fn = selectEventsFromResourceVersion(store.getState() as RootState);
49+
expect(fn('124')).toEqual([
4450
{ metadata: { name: 'event2', resourceVersion: '124' } },
4551
{ metadata: { name: 'event3', resourceVersion: '125' } },
4652
]);
4753
});
4854

4955
it('should return an empty array if resource version is invalid', () => {
50-
const selectFromResourceVersion = selectEventsFromResourceVersion(mockState);
51-
const result = selectFromResourceVersion('invalid');
52-
expect(result).toEqual([]);
56+
const store = new MockStoreBuilder()
57+
.withEvents({ events: mockState.events.events as never[], resourceVersion: '125' })
58+
.build();
59+
const fn = selectEventsFromResourceVersion(store.getState() as RootState);
60+
expect(fn('invalid')).toEqual([]);
5361
});
5462

5563
it('should select events error', () => {
56-
const result = selectEventsError(mockState);
57-
expect(result).toEqual(mockState.events.error);
64+
expect(selectEventsError(mockState)).toEqual(mockState.events.error);
5865
});
5966

6067
it('should select events resource version', () => {
61-
const result = selectEventsResourceVersion(mockState);
62-
expect(result).toEqual(mockState.events.resourceVersion);
68+
expect(selectEventsResourceVersion(mockState)).toEqual(mockState.events.resourceVersion);
6369
});
6470

65-
describe('workspace filtering', () => {
66-
// Real DevSpaces naming: workspace name = 'my-project',
67-
// workspace id = 'workspaceabc123' (derived from UID), pod = 'workspaceabc123-<rs>-<pod>'
68-
const workspaceId = 'workspaceabc123';
69-
const workspaceName = 'my-project';
70-
71-
const stateWithMixedEvents = {
72-
events: {
73-
events: [
74-
{
75-
metadata: { resourceVersion: '100' },
76-
involvedObject: { name: workspaceName },
77-
message: 'devworkspace event',
78-
},
79-
{
80-
metadata: { resourceVersion: '101' },
81-
involvedObject: { name: `${workspaceId}-6bcbf9f88-xyz01` },
82-
message: 'pod event',
83-
},
84-
{
85-
metadata: { resourceVersion: '102' },
86-
involvedObject: { name: 'other-workspace' },
87-
message: 'other workspace devworkspace event',
88-
},
89-
{
90-
metadata: { resourceVersion: '103' },
91-
involvedObject: { name: 'workspaceother999-abc-def' },
92-
message: 'other workspace pod event',
93-
},
94-
],
95-
error: undefined,
96-
resourceVersion: '103',
71+
describe('workspace filtering — blocklist approach', () => {
72+
// Build two real DevWorkspace objects via DevWorkspaceBuilder
73+
const currentDW = new DevWorkspaceBuilder()
74+
.withId('workspaceabc')
75+
.withName('my-workspace')
76+
.withNamespace('user-che')
77+
.build();
78+
const otherDW = new DevWorkspaceBuilder()
79+
.withId('workspaceXYZ')
80+
.withName('other-workspace')
81+
.withNamespace('user-che')
82+
.build();
83+
84+
const currentWorkspaceId = WorkspaceAdapter.getId(currentDW);
85+
const currentWorkspaceName = 'my-workspace';
86+
87+
const events = [
88+
// current workspace — DevWorkspace CR event
89+
{
90+
metadata: { resourceVersion: '100' },
91+
involvedObject: { name: 'my-workspace' },
92+
message: 'devworkspace event',
93+
},
94+
// current workspace — Deployment event
95+
{
96+
metadata: { resourceVersion: '101' },
97+
involvedObject: { name: currentWorkspaceId },
98+
message: 'deployment event',
99+
},
100+
// current workspace — ReplicaSet event
101+
{
102+
metadata: { resourceVersion: '102' },
103+
involvedObject: { name: currentWorkspaceId + '-7867c75d84' },
104+
message: 'replicaset event',
105+
},
106+
// current workspace — Pod event
107+
{
108+
metadata: { resourceVersion: '103' },
109+
involvedObject: { name: currentWorkspaceId + '-7867c75d84-kwb97' },
110+
message: 'pod event',
97111
},
98-
} as RootState;
112+
// generic cluster event (not tied to any workspace)
113+
{
114+
metadata: { resourceVersion: '104' },
115+
involvedObject: { name: 'some-other-resource' },
116+
message: 'generic event',
117+
},
118+
// other workspace — DevWorkspace CR event
119+
{
120+
metadata: { resourceVersion: '105' },
121+
involvedObject: { name: 'other-workspace' },
122+
message: 'other devworkspace event',
123+
},
124+
// other workspace — pod event
125+
{
126+
metadata: { resourceVersion: '106' },
127+
involvedObject: { name: WorkspaceAdapter.getId(otherDW) + '-abc123-pod1' },
128+
message: 'other pod event',
129+
},
130+
];
131+
132+
let state: RootState;
133+
beforeEach(() => {
134+
state = new MockStoreBuilder()
135+
.withDevWorkspaces({ workspaces: [currentDW, otherDW] })
136+
.withEvents({ events: events as never[], resourceVersion: '106' })
137+
.build()
138+
.getState() as RootState;
139+
});
140+
141+
it('should show current workspace DevWorkspace events', () => {
142+
const result = selectEventsFromResourceVersion(state)(
143+
'100',
144+
currentWorkspaceId,
145+
currentWorkspaceName,
146+
);
147+
expect(result.map(e => (e as { message: string }).message)).toContain('devworkspace event');
148+
});
149+
150+
it('should show current workspace Deployment events', () => {
151+
const result = selectEventsFromResourceVersion(state)(
152+
'100',
153+
currentWorkspaceId,
154+
currentWorkspaceName,
155+
);
156+
expect(result.map(e => (e as { message: string }).message)).toContain('deployment event');
157+
});
158+
159+
it('should show current workspace ReplicaSet events', () => {
160+
const result = selectEventsFromResourceVersion(state)(
161+
'100',
162+
currentWorkspaceId,
163+
currentWorkspaceName,
164+
);
165+
expect(result.map(e => (e as { message: string }).message)).toContain('replicaset event');
166+
});
167+
168+
it('should show current workspace Pod events', () => {
169+
const result = selectEventsFromResourceVersion(state)(
170+
'100',
171+
currentWorkspaceId,
172+
currentWorkspaceName,
173+
);
174+
expect(result.map(e => (e as { message: string }).message)).toContain('pod event');
175+
});
99176

100-
it('should include DevWorkspace events matched by workspace name', () => {
101-
const fn = selectEventsFromResourceVersion(stateWithMixedEvents);
102-
const result = fn('100', workspaceId, workspaceName);
103-
expect(result.map(e => e.message)).toContain('devworkspace event');
177+
it('should show generic cluster events not tied to any workspace', () => {
178+
const result = selectEventsFromResourceVersion(state)(
179+
'100',
180+
currentWorkspaceId,
181+
currentWorkspaceName,
182+
);
183+
expect(result.map(e => (e as { message: string }).message)).toContain('generic event');
104184
});
105185

106-
it('should include pod events matched by workspace id prefix', () => {
107-
const fn = selectEventsFromResourceVersion(stateWithMixedEvents);
108-
const result = fn('100', workspaceId, workspaceName);
109-
expect(result.map(e => e.message)).toContain('pod event');
186+
it('should hide other workspace DevWorkspace events', () => {
187+
const result = selectEventsFromResourceVersion(state)(
188+
'100',
189+
currentWorkspaceId,
190+
currentWorkspaceName,
191+
);
192+
expect(result.map(e => (e as { message: string }).message)).not.toContain(
193+
'other devworkspace event',
194+
);
110195
});
111196

112-
it('should exclude events from a different workspace', () => {
113-
const fn = selectEventsFromResourceVersion(stateWithMixedEvents);
114-
const result = fn('100', workspaceId, workspaceName);
115-
expect(result.map(e => e.message)).not.toContain('other workspace devworkspace event');
116-
expect(result.map(e => e.message)).not.toContain('other workspace pod event');
197+
it('should hide other workspace Pod events', () => {
198+
const result = selectEventsFromResourceVersion(state)(
199+
'100',
200+
currentWorkspaceId,
201+
currentWorkspaceName,
202+
);
203+
expect(result.map(e => (e as { message: string }).message)).not.toContain('other pod event');
117204
});
118205
});
119206
});

packages/dashboard-frontend/src/store/Events/selectors.ts

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,56 @@
1313
import { createSelector } from '@reduxjs/toolkit';
1414

1515
import { RootState } from '@/store';
16+
import { selectAllWorkspaces } from '@/store/Workspaces/selectors';
1617

1718
const selectState = (state: RootState) => state.events;
1819

1920
export const selectAllEvents = createSelector(selectState, state => state.events);
2021

21-
export const selectEventsFromResourceVersion = createSelector(selectAllEvents, allEvents => {
22-
return (fromResourceVersionStr: string, workspaceId?: string, workspaceName?: string) => {
23-
return allEvents.filter(event => {
24-
if (event.metadata.resourceVersion === undefined) {
25-
return false;
26-
}
27-
const resourceVersion = parseInt(event.metadata.resourceVersion, 10);
28-
const fromResourceVersion = parseInt(fromResourceVersionStr, 10);
29-
if (isNaN(resourceVersion) || isNaN(fromResourceVersion)) {
30-
return false;
31-
}
32-
if (fromResourceVersion > resourceVersion) {
33-
return false;
34-
}
35-
if (workspaceId !== undefined || workspaceName !== undefined) {
22+
export const selectEventsFromResourceVersion = createSelector(
23+
selectAllEvents,
24+
selectAllWorkspaces,
25+
(allEvents, allWorkspaces) => {
26+
// Pre-build the sets of all workspace IDs and names once per selector invocation.
27+
const allWorkspaceIds = allWorkspaces.map(w => w.id);
28+
const allWorkspaceNames = allWorkspaces.map(w => w.name);
29+
30+
return (
31+
fromResourceVersionStr: string,
32+
currentWorkspaceId?: string,
33+
currentWorkspaceName?: string,
34+
) => {
35+
return allEvents.filter(event => {
36+
if (event.metadata.resourceVersion === undefined) {
37+
return false;
38+
}
39+
const resourceVersion = parseInt(event.metadata.resourceVersion, 10);
40+
const fromResourceVersion = parseInt(fromResourceVersionStr, 10);
41+
if (isNaN(resourceVersion) || isNaN(fromResourceVersion)) {
42+
return false;
43+
}
44+
if (fromResourceVersion > resourceVersion) {
45+
return false;
46+
}
47+
48+
if (currentWorkspaceId === undefined) {
49+
return true;
50+
}
51+
52+
// Blocklist approach: hide events that belong to a DIFFERENT workspace.
53+
// Events for the current workspace (id-prefix match or exact name match) and
54+
// events not associated with any devworkspace are all kept.
3655
const objName = event.involvedObject?.name ?? '';
37-
// DevWorkspace resource events use workspace.name; pod/PVC events use
38-
// workspace.id (e.g. "workspaceabc123") as a prefix
39-
const matchesName = workspaceName !== undefined && objName === workspaceName;
40-
const matchesId = workspaceId !== undefined && objName.startsWith(workspaceId);
41-
return matchesName || matchesId;
42-
}
43-
return true;
44-
});
45-
};
46-
});
56+
57+
const belongsToOtherWorkspace =
58+
allWorkspaceIds.some(id => id !== currentWorkspaceId && objName.startsWith(id)) ||
59+
allWorkspaceNames.some(name => name !== currentWorkspaceName && objName === name);
60+
61+
return !belongsToOtherWorkspace;
62+
});
63+
};
64+
},
65+
);
4766

4867
export const selectEventsError = createSelector(selectState, state => state.error);
4968

0 commit comments

Comments
 (0)