Skip to content

Commit be24bec

Browse files
committed
added timeline-view and its timeline filters
1 parent 4f9bb49 commit be24bec

9 files changed

Lines changed: 1322 additions & 0 deletions

File tree

web/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@bufbuild/protobuf": "^2.11.0",
2424
"@fontsource/google-sans-code": "^5.2.3",
2525
"@fontsource/roboto": "^5.2.10",
26+
"@marcbachmann/cel-js": "^7.6.1",
2627
"angular-split": "^20.0.0",
2728
"golden-layout": "^2.6.0",
2829
"highlight.js": "^11.11.1",
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/**
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {
18+
CelTimelineFilter,
19+
CelLogFilter,
20+
} from 'src/app/store/domain/filter/cel-filter';
21+
import { LogTimelineFilterContext } from 'src/app/store/domain/filter/types';
22+
import { TimelineStore } from 'src/app/store/domain/timeline-store';
23+
import { LogStore } from 'src/app/store/domain/log-store';
24+
import { ReadonlyDomainElement } from 'src/app/store/domain/types';
25+
import { Timeline } from 'src/app/store/domain/timeline';
26+
import { Log } from 'src/app/store/domain/log';
27+
28+
describe('CelTimelineFilter', () => {
29+
it('should filter timelines based on configured CEL expression', () => {
30+
const timelines = [
31+
{
32+
id: 1,
33+
name: 'T1',
34+
type: { label: 'type1' },
35+
events: [],
36+
revisions: [],
37+
},
38+
{
39+
id: 2,
40+
name: 'T2',
41+
type: { label: 'type2' },
42+
events: [],
43+
revisions: [],
44+
},
45+
];
46+
const timelineStoreSpy = jasmine.createSpyObj<TimelineStore>(
47+
'TimelineStore',
48+
['getTimeline'],
49+
);
50+
timelineStoreSpy.getTimeline.and.callFake((id: number) => {
51+
const found = timelines.find((t) => t.id === id);
52+
if (!found) {
53+
throw new Error(`Timeline ${id} not found`);
54+
}
55+
return found as unknown as ReadonlyDomainElement<Timeline>;
56+
});
57+
58+
const filter = new CelTimelineFilter();
59+
const res = filter.updateFilter("timeline.name == 'T1'");
60+
expect(res.success).toBe(true);
61+
62+
const context: LogTimelineFilterContext = {
63+
timelineIds: new Set([1, 2]),
64+
logIds: new Set(),
65+
};
66+
67+
const result = filter.process(context, timelineStoreSpy);
68+
expect(result.timelineIds.size).toBe(1);
69+
expect(result.timelineIds.has(1)).toBe(true);
70+
});
71+
72+
it('should return original context if filter is not updated with an expression', () => {
73+
const filter = new CelTimelineFilter();
74+
const context: LogTimelineFilterContext = {
75+
timelineIds: new Set([1, 2]),
76+
logIds: new Set(),
77+
};
78+
const timelineStoreSpy = jasmine.createSpyObj<TimelineStore>(
79+
'TimelineStore',
80+
['getTimeline'],
81+
);
82+
83+
const result = filter.process(context, timelineStoreSpy);
84+
expect(result).toBe(context);
85+
});
86+
87+
it('should return error and not filter context when updateFilter is called with an invalid expression', () => {
88+
const filter = new CelTimelineFilter();
89+
const res = filter.updateFilter("timeline.name == 'T1");
90+
expect(res.success).toBe(false);
91+
expect(res.error).toBeDefined();
92+
93+
const context: LogTimelineFilterContext = {
94+
timelineIds: new Set([1, 2]),
95+
logIds: new Set(),
96+
};
97+
const timelineStoreSpy = jasmine.createSpyObj<TimelineStore>(
98+
'TimelineStore',
99+
['getTimeline'],
100+
);
101+
102+
const result = filter.process(context, timelineStoreSpy);
103+
expect(result).toBe(context);
104+
});
105+
106+
it('should reset evaluator and return original context if an invalid expression is provided after a valid one', () => {
107+
const filter = new CelTimelineFilter();
108+
filter.updateFilter("timeline.name == 'T1'");
109+
110+
const res = filter.updateFilter("timeline.name == 'T1");
111+
expect(res.success).toBe(false);
112+
113+
const context: LogTimelineFilterContext = {
114+
timelineIds: new Set([1, 2]),
115+
logIds: new Set(),
116+
};
117+
const timelineStoreSpy = jasmine.createSpyObj<TimelineStore>(
118+
'TimelineStore',
119+
['getTimeline'],
120+
);
121+
122+
const result = filter.process(context, timelineStoreSpy);
123+
expect(result).toBe(context);
124+
});
125+
});
126+
127+
describe('CelLogFilter', () => {
128+
it('should filter logs based on configured CEL expression', () => {
129+
const logs = [
130+
{
131+
id: 1,
132+
summary: 'L1',
133+
logType: { label: 'type1' },
134+
severity: { label: 'sev1' },
135+
},
136+
{
137+
id: 2,
138+
summary: 'L2',
139+
logType: { label: 'type2' },
140+
severity: { label: 'sev2' },
141+
},
142+
];
143+
const logStoreSpy = jasmine.createSpyObj<LogStore>('LogStore', ['getLog']);
144+
logStoreSpy.getLog.and.callFake((id: number) => {
145+
const found = logs.find((l) => l.id === id);
146+
if (!found) {
147+
throw new Error(`Log ${id} not found`);
148+
}
149+
return found as unknown as ReadonlyDomainElement<Log>;
150+
});
151+
const timelineStoreSpy = jasmine.createSpyObj<TimelineStore>(
152+
'TimelineStore',
153+
['getTimeline'],
154+
);
155+
Object.defineProperty(timelineStoreSpy, 'logStore', {
156+
get: () => logStoreSpy,
157+
});
158+
159+
const filter = new CelLogFilter();
160+
const res = filter.updateFilter("log.summary == 'L1'");
161+
expect(res.success).toBe(true);
162+
163+
const context: LogTimelineFilterContext = {
164+
timelineIds: new Set(),
165+
logIds: new Set([1, 2]),
166+
};
167+
168+
const result = filter.process(context, timelineStoreSpy);
169+
expect(result.logIds.size).toBe(1);
170+
expect(result.logIds.has(1)).toBe(true);
171+
});
172+
173+
it('should return original context if filter is not updated with an expression', () => {
174+
const filter = new CelLogFilter();
175+
const context: LogTimelineFilterContext = {
176+
timelineIds: new Set(),
177+
logIds: new Set([1, 2]),
178+
};
179+
const timelineStoreSpy = jasmine.createSpyObj<TimelineStore>(
180+
'TimelineStore',
181+
['getTimeline'],
182+
);
183+
184+
const result = filter.process(context, timelineStoreSpy);
185+
expect(result).toBe(context);
186+
});
187+
188+
it('should return error and not filter context when updateFilter is called with an invalid expression', () => {
189+
const filter = new CelLogFilter();
190+
const res = filter.updateFilter("log.summary == 'L1");
191+
expect(res.success).toBe(false);
192+
expect(res.error).toBeDefined();
193+
194+
const context: LogTimelineFilterContext = {
195+
timelineIds: new Set(),
196+
logIds: new Set([1, 2]),
197+
};
198+
const timelineStoreSpy = jasmine.createSpyObj<TimelineStore>(
199+
'TimelineStore',
200+
['getTimeline'],
201+
);
202+
203+
const result = filter.process(context, timelineStoreSpy);
204+
expect(result).toBe(context);
205+
});
206+
207+
it('should reset evaluator and return original context if an invalid expression is provided after a valid one', () => {
208+
const filter = new CelLogFilter();
209+
filter.updateFilter("log.summary == 'L1'");
210+
211+
const res = filter.updateFilter("log.summary == 'L1");
212+
expect(res.success).toBe(false);
213+
214+
const context: LogTimelineFilterContext = {
215+
timelineIds: new Set(),
216+
logIds: new Set([1, 2]),
217+
};
218+
const timelineStoreSpy = jasmine.createSpyObj<TimelineStore>(
219+
'TimelineStore',
220+
['getTimeline'],
221+
);
222+
223+
const result = filter.process(context, timelineStoreSpy);
224+
expect(result).toBe(context);
225+
});
226+
227+
it('should allow comparing severity with registered constants', () => {
228+
const logs = [
229+
{
230+
id: 1,
231+
summary: 'Info Log',
232+
logType: { label: 'type1' },
233+
severity: { label: 'INFO' },
234+
},
235+
{
236+
id: 2,
237+
summary: 'Error Log',
238+
logType: { label: 'type2' },
239+
severity: { label: 'ERROR' },
240+
},
241+
];
242+
const logStoreSpy = jasmine.createSpyObj<LogStore>('LogStore', ['getLog']);
243+
logStoreSpy.getLog.and.callFake((id: number) => {
244+
const found = logs.find((l) => l.id === id);
245+
if (!found) {
246+
throw new Error(`Log ${id} not found`);
247+
}
248+
return found as unknown as ReadonlyDomainElement<Log>;
249+
});
250+
const timelineStoreSpy = jasmine.createSpyObj<TimelineStore>(
251+
'TimelineStore',
252+
['getTimeline'],
253+
);
254+
Object.defineProperty(timelineStoreSpy, 'logStore', {
255+
get: () => logStoreSpy,
256+
});
257+
258+
const filter = new CelLogFilter();
259+
const res = filter.updateFilter('log.severity >= ERROR');
260+
expect(res.success).toBe(true);
261+
262+
const context: LogTimelineFilterContext = {
263+
timelineIds: new Set(),
264+
logIds: new Set([1, 2]),
265+
};
266+
267+
const result = filter.process(context, timelineStoreSpy);
268+
expect(result.logIds.size).toBe(1);
269+
expect(result.logIds.has(2)).toBe(true);
270+
});
271+
});

0 commit comments

Comments
 (0)