Skip to content

Commit 744801b

Browse files
committed
move files to new frontend directory
1 parent 29bb9a2 commit 744801b

11 files changed

Lines changed: 283 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<app-common-section-card *ngIf="selectedExperiment$ | async as experiment">
2+
<!-- Search Widget -->
3+
<app-common-section-card-search-header
4+
header-left
5+
[filterOptions]="(filterOptions$ | async) || []"
6+
[searchString]="(searchString$ | async) || ''"
7+
[searchKey]="(searchKey$ | async) || 'All'"
8+
(search)="onSearch($event)"
9+
>
10+
</app-common-section-card-search-header>
11+
12+
<!-- Action Buttons -->
13+
<app-common-section-card-action-buttons
14+
header-right
15+
[showPrimaryButton]="false"
16+
[isSectionCardExpanded]="isSectionCardExpanded"
17+
(sectionCardExpandChange)="onSectionCardExpandChange($event)"
18+
>
19+
</app-common-section-card-action-buttons>
20+
21+
<!-- Timeline Content -->
22+
<ng-container content *ngIf="isSectionCardExpanded">
23+
<common-audit-log-timeline
24+
[groupedLogs]="timelineDataSource$ | async"
25+
[isLoading]="isLoading$ | async"
26+
[isEmpty]="(experimentLogs$ | async)?.length === 0"
27+
[config]="timelineConfig"
28+
(scrolledToBottom)="fetchLogsOnScroll()"
29+
>
30+
</common-audit-log-timeline>
31+
</ng-container>
32+
</app-common-section-card>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { ChangeDetectionStrategy, Component, Input, OnInit, OnDestroy } from '@angular/core';
2+
import {
3+
CommonSectionCardActionButtonsComponent,
4+
CommonSectionCardComponent,
5+
CommonSectionCardSearchHeaderComponent,
6+
} from '../../../../../../../shared-standalone-component-lib/components';
7+
import {
8+
FilterOption,
9+
CommonSearchWidgetSearchParams,
10+
} from '../../../../../../../shared-standalone-component-lib/components/common-section-card-search-header/common-section-card-search-header.component';
11+
import { CommonModule } from '@angular/common';
12+
import { TranslateModule } from '@ngx-translate/core';
13+
import { ExperimentService } from '../../../../../../../core/experiments/experiments.service';
14+
import { Experiment } from '../../../../../../../core/experiments/store/experiments.model';
15+
import { LogsService } from '../../../../../../../core/logs/logs.service';
16+
import { SharedModule } from '../../../../../../../shared/shared.module';
17+
import { CommonAuditLogTimelineComponent } from '../../../../../../../shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component';
18+
import { AuditLogs, LogDateFormatType } from '../../../../../../../core/logs/store/logs.model';
19+
import { LOG_TYPE } from 'upgrade_types';
20+
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
21+
import { combineLatest, Subject, Observable, BehaviorSubject } from 'rxjs';
22+
import { map, filter, takeUntil, switchMap, tap, take } from 'rxjs/operators';
23+
import { groupBy } from 'lodash';
24+
import { AuditLogTimelineConfig } from '../../../../../../../shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline-config.model';
25+
import { EXPERIMENT_TIMELINE_LOG_TYPE_CONFIG } from '../../../../../../../shared-standalone-component-lib/components/common-audit-log-timeline/configs/experiment-timeline.config';
26+
27+
/**
28+
* Section card component for displaying experiment-specific audit logs in a timeline format.
29+
* Features:
30+
* - Dynamic filter dropdown with action types and users from logs
31+
* - Text search capability
32+
* - Timeline view grouped by date
33+
* - Infinite scroll pagination
34+
*/
35+
@Component({
36+
selector: 'app-experiment-log-section-card',
37+
imports: [
38+
CommonModule,
39+
CommonSectionCardComponent,
40+
CommonSectionCardActionButtonsComponent,
41+
CommonSectionCardSearchHeaderComponent,
42+
TranslateModule,
43+
SharedModule,
44+
CommonAuditLogTimelineComponent,
45+
MatProgressSpinnerModule,
46+
],
47+
standalone: true,
48+
templateUrl: './experiment-log-section-card.component.html',
49+
changeDetection: ChangeDetectionStrategy.OnPush,
50+
})
51+
export class ExperimentLogSectionCardComponent implements OnInit, OnDestroy {
52+
@Input() isSectionCardExpanded = true;
53+
54+
selectedExperiment$: Observable<Experiment> = this.experimentService.selectedExperiment$;
55+
56+
// Search state
57+
searchString$ = new BehaviorSubject<string>('');
58+
searchKey$ = new BehaviorSubject<string>('All');
59+
filterOptions$ = new BehaviorSubject<FilterOption[]>([{ value: 'All' }]);
60+
61+
// Logs data
62+
experimentLogs$: Observable<AuditLogs[]>;
63+
timelineDataSource$: Observable<{ dates: string[]; dateGroups: Record<string, AuditLogs[]> }>;
64+
isLoading$: Observable<boolean>;
65+
allLogsFetched$: Observable<boolean>;
66+
67+
private destroy$ = new Subject<void>();
68+
private currentExperimentId: string | null = null;
69+
70+
LogDateFormatType = LogDateFormatType;
71+
timelineConfig: AuditLogTimelineConfig = EXPERIMENT_TIMELINE_LOG_TYPE_CONFIG;
72+
73+
constructor(private readonly experimentService: ExperimentService, private readonly logsService: LogsService) {}
74+
75+
ngOnInit(): void {
76+
// Fetch logs when experiment loads
77+
this.selectedExperiment$
78+
.pipe(
79+
filter((exp): exp is Experiment => !!exp),
80+
tap((exp) => {
81+
this.currentExperimentId = exp.id;
82+
this.logsService.fetchExperimentLogs(exp.id, true);
83+
}),
84+
takeUntil(this.destroy$)
85+
)
86+
.subscribe();
87+
88+
// Get raw logs observable
89+
this.experimentLogs$ = this.selectedExperiment$.pipe(
90+
filter((exp): exp is Experiment => !!exp),
91+
switchMap((exp) => this.logsService.getExperimentLogsById(exp.id)),
92+
tap((logs) => {
93+
this.buildFilterOptions(logs);
94+
}),
95+
takeUntil(this.destroy$)
96+
);
97+
98+
// Get loading state
99+
this.isLoading$ = this.selectedExperiment$.pipe(
100+
filter((exp): exp is Experiment => !!exp),
101+
switchMap((exp) => this.logsService.getExperimentLogsLoadingState(exp.id)),
102+
takeUntil(this.destroy$)
103+
);
104+
105+
// Get pagination state
106+
this.allLogsFetched$ = this.selectedExperiment$.pipe(
107+
filter((exp): exp is Experiment => !!exp),
108+
switchMap((exp) => this.logsService.isAllExperimentLogsFetched(exp.id)),
109+
takeUntil(this.destroy$)
110+
);
111+
112+
// Apply search and group by date
113+
// TODO: prefer doing this on backend
114+
this.timelineDataSource$ = combineLatest([this.experimentLogs$, this.searchString$, this.searchKey$]).pipe(
115+
map(([logs, searchString, searchKey]) => {
116+
// TODO: prefer doing this on backend
117+
const filtered = this.filterLogs(logs, searchString, searchKey);
118+
const dateGroups = this.groupLogsByDate(filtered);
119+
const dates = Object.keys(dateGroups);
120+
return {
121+
dates,
122+
dateGroups,
123+
};
124+
}),
125+
takeUntil(this.destroy$)
126+
);
127+
}
128+
129+
ngOnDestroy(): void {
130+
this.destroy$.next();
131+
this.destroy$.complete();
132+
}
133+
134+
onSectionCardExpandChange(isSectionCardExpanded: boolean): void {
135+
this.isSectionCardExpanded = isSectionCardExpanded;
136+
}
137+
138+
onSearch(params: CommonSearchWidgetSearchParams<string>): void {
139+
this.searchKey$.next(params.searchKey);
140+
this.searchString$.next(params.searchString);
141+
}
142+
143+
fetchLogsOnScroll(): void {
144+
if (!this.currentExperimentId) return;
145+
146+
// Check if all logs are fetched and if not currently loading
147+
combineLatest([this.allLogsFetched$, this.isLoading$])
148+
.pipe(
149+
take(1),
150+
filter(([allFetched, isLoading]) => !allFetched && !isLoading),
151+
takeUntil(this.destroy$)
152+
)
153+
.subscribe(() => {
154+
this.logsService.fetchExperimentLogs(this.currentExperimentId);
155+
});
156+
}
157+
158+
/**
159+
* Build dynamic filter options from logs data
160+
*/
161+
private buildFilterOptions(logs: AuditLogs[]): void {
162+
if (!logs || logs.length === 0) {
163+
this.filterOptions$.next([{ value: 'All' }]);
164+
return;
165+
}
166+
167+
// Get unique action types (only experiment-related)
168+
const experimentLogTypes = [
169+
LOG_TYPE.EXPERIMENT_CREATED,
170+
LOG_TYPE.EXPERIMENT_UPDATED,
171+
LOG_TYPE.EXPERIMENT_STATE_CHANGED,
172+
LOG_TYPE.EXPERIMENT_DELETED,
173+
LOG_TYPE.EXPERIMENT_DATA_EXPORTED,
174+
LOG_TYPE.EXPERIMENT_DESIGN_EXPORTED,
175+
];
176+
177+
const actionTypes = [...new Set(logs.map((log) => log.type))].filter((type) => experimentLogTypes.includes(type));
178+
179+
// Get unique users (firstName + lastName)
180+
const userSet = new Set<string>();
181+
logs.forEach((log) => {
182+
if (log.user?.firstName && log.user?.lastName) {
183+
userSet.add(`${log.user.firstName} ${log.user.lastName}`);
184+
}
185+
});
186+
const users = Array.from(userSet);
187+
188+
// Build grouped filter options
189+
const options: FilterOption[] = [
190+
{ value: 'All' }, // Standalone option
191+
...actionTypes.map((type) => ({
192+
value: type,
193+
group: 'Event Type', // Group name
194+
})),
195+
];
196+
197+
if (users.length > 0) {
198+
users.forEach((user) => {
199+
options.push({ value: user, group: 'Users' }); // Group name
200+
});
201+
}
202+
203+
this.filterOptions$.next(options);
204+
}
205+
206+
/**
207+
* Filter logs based on search criteria
208+
*/
209+
private filterLogs(logs: AuditLogs[], searchString: string, searchKey: string): AuditLogs[] {
210+
let filtered = logs;
211+
212+
// Apply dropdown filter
213+
if (searchKey && searchKey !== 'All') {
214+
// Check if it's an action type
215+
if (Object.values(LOG_TYPE).includes(searchKey as LOG_TYPE)) {
216+
filtered = filtered.filter((log) => log.type === searchKey);
217+
}
218+
// Check if it's a user name
219+
else {
220+
filtered = filtered.filter((log) => {
221+
const userName = `${log.user?.firstName || ''} ${log.user?.lastName || ''}`.trim();
222+
return userName === searchKey;
223+
});
224+
}
225+
}
226+
227+
// Apply text search
228+
if (searchString) {
229+
const searchLower = searchString.toLowerCase();
230+
filtered = filtered.filter((log) => {
231+
const userName = `${log.user?.firstName || ''} ${log.user?.lastName || ''}`.toLowerCase();
232+
const actionType = log.type.toLowerCase();
233+
const dataStr = JSON.stringify(log.data).toLowerCase();
234+
235+
return userName.includes(searchLower) || actionType.includes(searchLower) || dataStr.includes(searchLower);
236+
});
237+
}
238+
239+
return filtered;
240+
}
241+
242+
/**
243+
* Group logs by date (format: YYYY/M/D)
244+
*/
245+
private groupLogsByDate(logs: AuditLogs[]): Record<string, AuditLogs[]> {
246+
return groupBy(logs, (log) => {
247+
const date = new Date(log.createdAt);
248+
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
249+
});
250+
}
251+
}

frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-diff-display/common-audit-log-diff-display.component.html renamed to packages/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-diff-display/common-audit-log-diff-display.component.html

File renamed without changes.

frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-diff-display/common-audit-log-diff-display.component.scss renamed to packages/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-diff-display/common-audit-log-diff-display.component.scss

File renamed without changes.

frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-diff-display/common-audit-log-diff-display.component.ts renamed to packages/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-diff-display/common-audit-log-diff-display.component.ts

File renamed without changes.

frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline-config.model.ts renamed to packages/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline-config.model.ts

File renamed without changes.

frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.html renamed to packages/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.html

File renamed without changes.

frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.scss renamed to packages/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.scss

File renamed without changes.

frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.ts renamed to packages/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.ts

File renamed without changes.

frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/configs/experiment-timeline.config.ts renamed to packages/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/configs/experiment-timeline.config.ts

File renamed without changes.

0 commit comments

Comments
 (0)