From 925661ae54940353f45ddbdbc20a001173b07a85 Mon Sep 17 00:00:00 2001 From: doswalt Date: Tue, 3 Mar 2026 10:58:23 -0500 Subject: [PATCH 1/2] refactor to make timeline component generalizeable --- .../validators/AuditLogParamsValidators.ts | 2 +- .../upgrade/src/app/core/logs/logs.service.ts | 2 +- .../src/app/core/logs/store/logs.model.ts | 13 +- .../src/app/core/logs/store/logs.reducer.ts | 30 ++--- .../src/app/core/logs/store/logs.selectors.ts | 2 +- ...experiment-log-section-card.component.html | 5 +- .../experiment-log-section-card.component.ts | 9 +- .../experiment-logs-timeline.component.ts | 113 ------------------ .../components/timeline/timeline.component.ts | 4 +- ...mon-audit-log-diff-display.component.html} | 0 ...mon-audit-log-diff-display.component.scss} | 0 ...ommon-audit-log-diff-display.component.ts} | 14 +-- .../common-audit-log-timeline-config.model.ts | 91 ++++++++++++++ .../common-audit-log-timeline.component.html} | 78 ++++++------ .../common-audit-log-timeline.component.scss} | 0 .../common-audit-log-timeline.component.ts | 103 ++++++++++++++++ .../configs/experiment-timeline.config.ts | 43 +++++++ .../configs/feature-flag-timeline.config.ts | 47 ++++++++ ...on-section-card-search-header.component.ts | 18 +-- .../components/index.ts | 4 + 20 files changed, 376 insertions(+), 202 deletions(-) delete mode 100644 frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-logs-timeline/experiment-logs-timeline.component.ts rename frontend/projects/upgrade/src/app/{features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-diff-display/experiment-log-diff-display.component.html => shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-diff-display/common-audit-log-diff-display.component.html} (100%) rename frontend/projects/upgrade/src/app/{features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-diff-display/experiment-log-diff-display.component.scss => shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-diff-display/common-audit-log-diff-display.component.scss} (100%) rename frontend/projects/upgrade/src/app/{features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-diff-display/experiment-log-diff-display.component.ts => shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-diff-display/common-audit-log-diff-display.component.ts} (77%) create mode 100644 frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline-config.model.ts rename frontend/projects/upgrade/src/app/{features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-logs-timeline/experiment-logs-timeline.component.html => shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.html} (70%) rename frontend/projects/upgrade/src/app/{features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-logs-timeline/experiment-logs-timeline.component.scss => shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.scss} (100%) create mode 100644 frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.ts create mode 100644 frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/configs/experiment-timeline.config.ts create mode 100644 frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/configs/feature-flag-timeline.config.ts diff --git a/backend/packages/Upgrade/src/api/controllers/validators/AuditLogParamsValidators.ts b/backend/packages/Upgrade/src/api/controllers/validators/AuditLogParamsValidators.ts index 76786576e4..83a84d2928 100644 --- a/backend/packages/Upgrade/src/api/controllers/validators/AuditLogParamsValidators.ts +++ b/backend/packages/Upgrade/src/api/controllers/validators/AuditLogParamsValidators.ts @@ -15,6 +15,6 @@ export class AuditLogParamsValidator { @IsOptional() @IsString() - @IsUUID('4') + @IsUUID() public experimentId?: string; } diff --git a/frontend/projects/upgrade/src/app/core/logs/logs.service.ts b/frontend/projects/upgrade/src/app/core/logs/logs.service.ts index b324820409..7ab5f580a4 100644 --- a/frontend/projects/upgrade/src/app/core/logs/logs.service.ts +++ b/frontend/projects/upgrade/src/app/core/logs/logs.service.ts @@ -118,7 +118,7 @@ export class LogsService { this.store$.dispatch(logsActions.actionSetErrorLogFilter({ filterType })); } - fetchExperimentLogsById(experimentId: string) { + getExperimentLogsById(experimentId: string) { return this.store$.pipe(select(selectExperimentLogs, { experimentId })); } diff --git a/frontend/projects/upgrade/src/app/core/logs/store/logs.model.ts b/frontend/projects/upgrade/src/app/core/logs/store/logs.model.ts index e80840e4e9..44123349bd 100644 --- a/frontend/projects/upgrade/src/app/core/logs/store/logs.model.ts +++ b/frontend/projects/upgrade/src/app/core/logs/store/logs.model.ts @@ -1,8 +1,10 @@ import { AppState } from '../../core.module'; import { EntityState } from '@ngrx/entity'; import { LOG_TYPE, SERVER_ERROR } from 'upgrade_types'; +import { User } from '../../users/store/users.model'; export const NUMBER_OF_LOGS = 20; +export const SYSTEM_USER_EMAIL = 'system@gmail.com'; export enum LogType { ERROR_LOG = 'Error log', @@ -34,12 +36,7 @@ export interface AuditLogs { versionNumber: number; type: LOG_TYPE; data: any; - user?: { - firstName: string; - lastName: string; - email?: string; - imageUrl?: string; - }; + user?: User; } export interface ErrorLogs { @@ -54,7 +51,7 @@ export interface ErrorLogs { name: string; } -export interface ExperimentLogsMetadata { +export interface AuditLogsMetadata { logs: AuditLogs[]; skip: number; total: number | null; @@ -71,7 +68,7 @@ export interface LogState extends EntityState { totalErrorLogs: number; auditLogFilter: LOG_TYPE; errorLogFilter: SERVER_ERROR; - experimentLogs: Record; + experimentAuditLogs: Record; } export interface State extends AppState { diff --git a/frontend/projects/upgrade/src/app/core/logs/store/logs.reducer.ts b/frontend/projects/upgrade/src/app/core/logs/store/logs.reducer.ts index b9767e4080..ac4134adb8 100644 --- a/frontend/projects/upgrade/src/app/core/logs/store/logs.reducer.ts +++ b/frontend/projects/upgrade/src/app/core/logs/store/logs.reducer.ts @@ -1,13 +1,13 @@ import { createReducer, on, Action } from '@ngrx/store'; import { createEntityAdapter, EntityAdapter } from '@ngrx/entity'; -import { AuditLogs, LogState, ErrorLogs, ExperimentLogsMetadata } from './logs.model'; +import { AuditLogs, LogState, ErrorLogs, AuditLogsMetadata } from './logs.model'; import * as logsActions from './logs.actions'; export const adapter: EntityAdapter = createEntityAdapter(); export const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); -const initialExperimentLogsMetadata: ExperimentLogsMetadata = { +const initialAuditLogsMetadata: AuditLogsMetadata = { logs: [], skip: 0, total: null, @@ -24,7 +24,7 @@ export const initialState: LogState = adapter.getInitialState({ totalErrorLogs: null, auditLogFilter: null, errorLogFilter: null, - experimentLogs: {}, + experimentAuditLogs: {}, }); const reducer = createReducer( @@ -67,11 +67,11 @@ const reducer = createReducer( on(logsActions.actionSetErrorLogFilter, (state, { filterType }) => ({ ...state, errorLogFilter: filterType })), // Experiment-specific log handlers on(logsActions.actionGetExperimentLogs, (state, { experimentId, fromStart }) => { - const experimentLog = state.experimentLogs[experimentId] || initialExperimentLogsMetadata; + const experimentLog = state.experimentAuditLogs[experimentId] || initialAuditLogsMetadata; return { ...state, - experimentLogs: { - ...state.experimentLogs, + experimentAuditLogs: { + ...state.experimentAuditLogs, [experimentId]: { ...experimentLog, isLoading: true, @@ -81,13 +81,13 @@ const reducer = createReducer( }; }), on(logsActions.actionGetExperimentLogsSuccess, (state, { experimentId, auditLogs, totalAuditLogs, fromStart }) => { - const experimentLog = state.experimentLogs[experimentId] || initialExperimentLogsMetadata; + const experimentLog = state.experimentAuditLogs[experimentId] || initialAuditLogsMetadata; const updatedLogs = fromStart ? auditLogs : [...experimentLog.logs, ...auditLogs]; return { ...state, - experimentLogs: { - ...state.experimentLogs, + experimentAuditLogs: { + ...state.experimentAuditLogs, [experimentId]: { ...experimentLog, logs: updatedLogs, @@ -99,11 +99,11 @@ const reducer = createReducer( }; }), on(logsActions.actionGetExperimentLogsFailure, (state, { experimentId }) => { - const experimentLog = state.experimentLogs[experimentId] || initialExperimentLogsMetadata; + const experimentLog = state.experimentAuditLogs[experimentId] || initialAuditLogsMetadata; return { ...state, - experimentLogs: { - ...state.experimentLogs, + experimentAuditLogs: { + ...state.experimentAuditLogs, [experimentId]: { ...experimentLog, isLoading: false, @@ -112,11 +112,11 @@ const reducer = createReducer( }; }), on(logsActions.actionSetExperimentLogFilter, (state, { experimentId, filterType }) => { - const experimentLog = state.experimentLogs[experimentId] || initialExperimentLogsMetadata; + const experimentLog = state.experimentAuditLogs[experimentId] || initialAuditLogsMetadata; return { ...state, - experimentLogs: { - ...state.experimentLogs, + experimentAuditLogs: { + ...state.experimentAuditLogs, [experimentId]: { ...experimentLog, filter: filterType, diff --git a/frontend/projects/upgrade/src/app/core/logs/store/logs.selectors.ts b/frontend/projects/upgrade/src/app/core/logs/store/logs.selectors.ts index 1bc25471be..7112a1caff 100644 --- a/frontend/projects/upgrade/src/app/core/logs/store/logs.selectors.ts +++ b/frontend/projects/upgrade/src/app/core/logs/store/logs.selectors.ts @@ -40,7 +40,7 @@ export const selectAuditFilterType = createSelector(selectLogState, (state) => s export const selectErrorFilterType = createSelector(selectLogState, (state) => state.errorLogFilter); // Experiment-specific log selectors -export const selectExperimentLogsState = createSelector(selectLogState, (state) => state.experimentLogs); +export const selectExperimentLogsState = createSelector(selectLogState, (state) => state.experimentAuditLogs); export const selectExperimentLogsMetadata = createSelector( selectExperimentLogsState, diff --git a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-section-card.component.html b/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-section-card.component.html index 6925e09ee4..ea089cab2f 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-section-card.component.html +++ b/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-section-card.component.html @@ -20,12 +20,13 @@ - - + diff --git a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-section-card.component.ts index 700172eb29..2d285439b3 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-section-card.component.ts @@ -14,13 +14,15 @@ import { ExperimentService } from '../../../../../../../core/experiments/experim import { Experiment } from '../../../../../../../core/experiments/store/experiments.model'; import { LogsService } from '../../../../../../../core/logs/logs.service'; import { SharedModule } from '../../../../../../../shared/shared.module'; -import { ExperimentLogsTimelineComponent } from './experiment-logs-timeline/experiment-logs-timeline.component'; +import { CommonAuditLogTimelineComponent } from '../../../../../../../shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component'; import { AuditLogs, LogDateFormatType } from '../../../../../../../core/logs/store/logs.model'; import { LOG_TYPE } from 'upgrade_types'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { combineLatest, Subject, Observable, BehaviorSubject } from 'rxjs'; import { map, filter, takeUntil, switchMap, tap, take } from 'rxjs/operators'; import { groupBy } from 'lodash'; +import { AuditLogTimelineConfig } from '../../../../../../../shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline-config.model'; +import { EXPERIMENT_TIMELINE_LOG_TYPE_CONFIG } from '../../../../../../../shared-standalone-component-lib/components/common-audit-log-timeline/configs/experiment-timeline.config'; /** * Section card component for displaying experiment-specific audit logs in a timeline format. @@ -39,7 +41,7 @@ import { groupBy } from 'lodash'; CommonSectionCardSearchHeaderComponent, TranslateModule, SharedModule, - ExperimentLogsTimelineComponent, + CommonAuditLogTimelineComponent, MatProgressSpinnerModule, ], standalone: true, @@ -67,6 +69,7 @@ export class ExperimentLogSectionCardComponent implements OnInit, OnDestroy { private currentExperimentId: string | null = null; LogDateFormatType = LogDateFormatType; + timelineConfig: AuditLogTimelineConfig = EXPERIMENT_TIMELINE_LOG_TYPE_CONFIG; constructor(private readonly experimentService: ExperimentService, private readonly logsService: LogsService) {} @@ -86,7 +89,7 @@ export class ExperimentLogSectionCardComponent implements OnInit, OnDestroy { // Get raw logs observable this.experimentLogs$ = this.selectedExperiment$.pipe( filter((exp): exp is Experiment => !!exp), - switchMap((exp) => this.logsService.fetchExperimentLogsById(exp.id)), + switchMap((exp) => this.logsService.getExperimentLogsById(exp.id)), tap((logs) => { this.buildFilterOptions(logs); }), diff --git a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-logs-timeline/experiment-logs-timeline.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-logs-timeline/experiment-logs-timeline.component.ts deleted file mode 100644 index f35fd2546e..0000000000 --- a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-logs-timeline/experiment-logs-timeline.component.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { TranslateModule } from '@ngx-translate/core'; -import { LOG_TYPE, EXPERIMENT_LIST_OPERATION } from 'upgrade_types'; -import { ExperimentLogDiffDisplayComponent } from '../experiment-log-diff-display/experiment-log-diff-display.component'; -import { User } from '../../../../../../../../core/users/store/users.model'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { SharedModule } from '../../../../../../../../shared/shared.module'; -import { AuditLogs, LogDateFormatType } from '../../../../../../../../core/logs/store/logs.model'; - -/** - * Timeline component for displaying experiment-specific audit logs. - */ -@Component({ - selector: 'app-experiment-logs-timeline', - templateUrl: './experiment-logs-timeline.component.html', - styleUrls: ['./experiment-logs-timeline.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - CommonModule, - RouterModule, - MatExpansionModule, - MatTooltipModule, - MatProgressSpinnerModule, - TranslateModule, - ExperimentLogDiffDisplayComponent, - SharedModule, - ], -}) -export class ExperimentLogsTimelineComponent { - @Input() groupedLogs: { dates: string[]; dateGroups: Record }; - @Input() isLoading = false; - @Input() isEmpty = false; - @Output() scrolledToBottom = new EventEmitter(); - - systemUserEmail = 'system@gmail.com'; - LogDateFormatType = LogDateFormatType; - - get ExperimentLogType() { - return LOG_TYPE; - } - - get EXPERIMENT_LIST_OPERATION() { - return EXPERIMENT_LIST_OPERATION; - } - - logTypeMessageMap = { - [LOG_TYPE.EXPERIMENT_CREATED]: 'logs.audit-log-experiment-created.text', - [LOG_TYPE.EXPERIMENT_DELETED]: 'logs.audit-log-experiment-deleted.text', - [LOG_TYPE.EXPERIMENT_STATE_CHANGED]: 'logs.audit-log-experiment-state-changed.text', - [LOG_TYPE.EXPERIMENT_UPDATED]: 'logs.audit-log-experiment-updated.text', - [LOG_TYPE.EXPERIMENT_DATA_EXPORTED]: 'logs.audit-log-experiment-data-exported.text', - [LOG_TYPE.EXPERIMENT_DESIGN_EXPORTED]: 'logs.audit-log-experiment-design-exported.text', - [LOG_TYPE.FEATURE_FLAG_CREATED]: 'logs.audit-log-feature-flag-created.text', - [LOG_TYPE.FEATURE_FLAG_DELETED]: 'logs.audit-log-feature-flag-deleted.text', - [LOG_TYPE.FEATURE_FLAG_STATUS_CHANGED]: 'logs.audit-log-feature-flag-state-changed.text', - [LOG_TYPE.FEATURE_FLAG_UPDATED]: 'logs.audit-log-feature-flag-updated.text', - [LOG_TYPE.FEATURE_FLAG_DATA_EXPORTED]: 'logs.audit-log-feature-flag-data-exported.text', - [LOG_TYPE.FEATURE_FLAG_DESIGN_EXPORTED]: 'logs.audit-log-feature-flag-design-exported.text', - }; - - listOperationMessageMap = { - [EXPERIMENT_LIST_OPERATION.CREATED]: 'logs.audit-log-list-created.text', - [EXPERIMENT_LIST_OPERATION.DELETED]: 'logs.audit-log-list-deleted.text', - [EXPERIMENT_LIST_OPERATION.UPDATED]: 'logs.audit-log-list-updated.text', - }; - - hasDiff(log: AuditLogs): boolean { - return !!(log.data?.diff || log.data?.list?.diff); - } - - // User-related helpers - getUserFullName(user: User): string { - if (!user?.firstName && !user?.lastName) return ''; - return `${user.firstName || ''} ${user.lastName || ''}`.trim(); - } - - isSystemUser(user: User): boolean { - return user?.email === this.systemUserEmail; - } - - hasUserImage(user: User): boolean { - return !!user?.imageUrl; - } - - shouldTruncateUserName(user: User): boolean { - return this.getUserFullName(user).length >= 30; - } - - isSimpleLogType(type: LOG_TYPE): boolean { - return [ - LOG_TYPE.EXPERIMENT_DELETED, - LOG_TYPE.EXPERIMENT_DATA_EXPORTED, - LOG_TYPE.EXPERIMENT_DESIGN_EXPORTED, - ].includes(type); - } - - isStateChangeOrCreated(type: LOG_TYPE): boolean { - return type === LOG_TYPE.EXPERIMENT_STATE_CHANGED || type === LOG_TYPE.EXPERIMENT_CREATED; - } - - getListTableType(filterType: string): string { - return filterType === 'inclusion' ? 'include' : 'exclude'; - } - - onScrolledToBottom(): void { - this.scrolledToBottom.emit(); - } -} diff --git a/frontend/projects/upgrade/src/app/features/dashboard/logs/components/timeline/timeline.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/logs/components/timeline/timeline.component.ts index 0f121b2b45..f2e3f8f4fb 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/logs/components/timeline/timeline.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/logs/components/timeline/timeline.component.ts @@ -1,6 +1,6 @@ import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; import * as env from '../../../../../../environments/environment'; -import { LogType } from '../../../../../core/logs/store/logs.model'; +import { LogType, SYSTEM_USER_EMAIL } from '../../../../../core/logs/store/logs.model'; import { EXPERIMENT_LIST_OPERATION, FEATURE_FLAG_LIST_OPERATION, LOG_TYPE, SERVER_ERROR } from 'upgrade_types'; import Convert from 'ansi-to-html'; @@ -15,7 +15,7 @@ export class TimelineComponent { @Input() logData; @Input() logType: LogType; // Used to change setting icon based on theme - systemUserEmail = 'system@gmail.com'; + systemUserEmail = SYSTEM_USER_EMAIL; endPoint = env.environment.apiBaseUrl.substring(0, env.environment.apiBaseUrl.lastIndexOf('/')); get LogType() { diff --git a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-diff-display/experiment-log-diff-display.component.html b/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 similarity index 100% rename from frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-diff-display/experiment-log-diff-display.component.html rename to 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 diff --git a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-diff-display/experiment-log-diff-display.component.scss b/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 similarity index 100% rename from frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-diff-display/experiment-log-diff-display.component.scss rename to 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 diff --git a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-diff-display/experiment-log-diff-display.component.ts b/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 similarity index 77% rename from frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-diff-display/experiment-log-diff-display.component.ts rename to 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 index a810e5c7b0..ca58c35263 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-log-diff-display/experiment-log-diff-display.component.ts +++ b/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 @@ -2,24 +2,24 @@ import { Component, ChangeDetectionStrategy, Input, AfterViewInit } from '@angul import { CommonModule } from '@angular/common'; import { MatExpansionModule } from '@angular/material/expansion'; import { TranslateModule } from '@ngx-translate/core'; -import { LOG_TYPE } from 'upgrade_types'; import Convert from 'ansi-to-html'; /** - * Sub-component for displaying diffs in experiment logs. + * Generic component for displaying diffs in audit logs. + * Supports any entity type (experiments, feature flags, segments). */ @Component({ - selector: 'app-experiment-log-diff-display', - templateUrl: './experiment-log-diff-display.component.html', - styleUrls: ['./experiment-log-diff-display.component.scss'], + selector: 'common-audit-log-diff-display', + templateUrl: './common-audit-log-diff-display.component.html', + styleUrls: ['./common-audit-log-diff-display.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [CommonModule, MatExpansionModule, TranslateModule], }) -export class ExperimentLogDiffDisplayComponent implements AfterViewInit { +export class CommonAuditLogDiffDisplayComponent implements AfterViewInit { @Input() logId: string; @Input() logData: any; - @Input() logType: LOG_TYPE; + @Input() logType: string; @Input() createdAt: string; @Input() actionMessage: string; diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline-config.model.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline-config.model.ts new file mode 100644 index 0000000000..5a90917fac --- /dev/null +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline-config.model.ts @@ -0,0 +1,91 @@ +/** + * Configuration interface for entity-specific timeline behavior. + * + * This model enables the CommonAuditLogTimelineComponent to display audit logs + * for different entity types (experiments, feature flags, segments) by providing + * entity-specific message maps and behavior functions. + * + * @example + * ```typescript + * const experimentConfig: TimelineLogTypeConfig = { + * logTypeMessageMap: { + * [LOG_TYPE.EXPERIMENT_CREATED]: 'logs.audit-log-experiment-created.text', + * ... + * }, + * isSimpleLogType: (type) => [LOG_TYPE.EXPERIMENT_DELETED, ...].includes(type), + * ... + * }; + * ``` + */ + +/** + * Entity-specific configuration for timeline log display. + */ +export interface AuditLogTimelineConfig { + /** + * Mapping of log type enum values to i18n translation keys. + * Used to display the appropriate message for each log action. + * + * Example: \{ 'EXPERIMENT_CREATED': 'logs.audit-log-experiment-created.text' \} + */ + logTypeMessageMap: Record; + + /** + * Optional mapping for list operation types (inclusion/exclusion lists). + * Only needed if the entity supports list operations. + * + * Example: \{ 'CREATED': 'logs.audit-log-list-created.text' \} + */ + listOperationMessageMap?: Record; + + /** + * Determines if a log type should be displayed as a simple message + * without expandable diff panels (e.g., deleted, exported logs). + * + * @param type - The log type as a string + * @returns true if the log should be displayed simply + */ + isSimpleLogType: (type: string) => boolean; + + /** + * Determines if a log type represents a state change or creation event. + * These logs may display additional state information (old state → new state). + * + * @param type - The log type as a string + * @returns true if the log is a state change or creation event + */ + isStateChangeOrCreated: (type: string) => boolean; + + /** + * Checks if the log data contains a list operation (inclusion/exclusion). + * + * @param logData - The log's data object + * @returns true if the log contains a list operation + */ + hasListOperation: (logData: any) => boolean; + + /** + * Determines if a log type represents an update operation. + * Update logs typically have expandable diff displays. + * + * @param type - The log type as a string + * @returns true if the log is an update operation + */ + isUpdateLogType?: (type: string) => boolean; +} + +/** + * Complete timeline configuration including system settings and entity-specific behavior. + */ +// export interface TimelineConfig { +// /** +// * Email address used to identify system-generated logs. +// * System logs are displayed with a special icon. +// */ +// systemUserEmail: string; + +// /** +// * Entity-specific configuration for log type handling and message display. +// */ +// entityConfig: TimelineLogTypeConfig; +// } diff --git a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-logs-timeline/experiment-logs-timeline.component.html b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.html similarity index 70% rename from frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-logs-timeline/experiment-logs-timeline.component.html rename to frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.html index fd9eda0035..e5d2e8b43d 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-logs-timeline/experiment-logs-timeline.component.html +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.html @@ -61,7 +61,7 @@ - {{ logTypeMessageMap[log.type] | translate }} + {{ getLogTypeMessage(log.type) | translate }} {{ log.createdAt | date : 'shortTime' }} @@ -69,9 +69,9 @@ - + {{ - logTypeMessageMap[log.type] + getLogTypeMessage(log.type) | translate : { previousState: log.data.previousState | titlecase, @@ -79,57 +79,55 @@ } }} - - {{ logTypeMessageMap[log.type] | translate }} + + {{ getLogTypeMessage(log.type) | translate }} {{ log.createdAt | date : 'shortTime' }} -
+
- + -
+
- {{ logTypeMessageMap[log.type] | translate }} - - - {{ - listOperationMessageMap[log.data.list.operation] - | translate - : { - listTableType: getListTableType(log.data.list.filterType), - listName: log.data.list.listName - } - }} + {{ getLogTypeMessage(log.type) | translate }} + + - + {{ + getListOperationMessage(log.data.list.operation) + | translate + : { + listTableType: getListTableType(log.data.list.filterType), + listName: log.data.list.listName + } + }} + {{ log.createdAt | date : 'shortTime' }}
-
+
- {{ logTypeMessageMap[log.type] | translate }} - - - {{ - listOperationMessageMap[log.data.list.operation] - | translate - : { - listTableType: getListTableType(log.data.list.filterType), - listName: log.data.list.listName - } - }} + {{ getLogTypeMessage(log.type) | translate }} + + - + {{ + getListOperationMessage(log.data.list.operation) + | translate + : { + listTableType: getListTableType(log.data.list.filterType), + listName: log.data.list.listName + } + }} + - - - + + + [actionMessage]="getLogTypeMessage(log.type) | translate" + >
diff --git a/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-logs-timeline/experiment-logs-timeline.component.scss b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.scss similarity index 100% rename from frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-log-section-card/experiment-logs-timeline/experiment-logs-timeline.component.scss rename to frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.scss diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.ts new file mode 100644 index 0000000000..1ce855fd6c --- /dev/null +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/common-audit-log-timeline.component.ts @@ -0,0 +1,103 @@ +import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +import { AuditLogTimelineConfig } from './common-audit-log-timeline-config.model'; +import { CommonAuditLogDiffDisplayComponent } from './common-audit-log-diff-display/common-audit-log-diff-display.component'; +import { SharedModule } from '../../../shared/shared.module'; +import { AuditLogs, LogDateFormatType, SYSTEM_USER_EMAIL } from '../../../core/logs/store/logs.model'; +import { User } from '../../../core/users/store/users.model'; + +/** + * Generic timeline component for displaying audit logs for any entity type. + * Configured via TimelineConfig to handle entity-specific behavior. + */ +@Component({ + selector: 'common-audit-log-timeline', + templateUrl: './common-audit-log-timeline.component.html', + styleUrls: ['./common-audit-log-timeline.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + RouterModule, + MatExpansionModule, + MatTooltipModule, + MatProgressSpinnerModule, + TranslateModule, + CommonAuditLogDiffDisplayComponent, + SharedModule, + ], +}) +export class CommonAuditLogTimelineComponent { + @Input() groupedLogs: { dates: string[]; dateGroups: Record }; + @Input() isLoading = false; + @Input() isEmpty = false; + @Input() config: AuditLogTimelineConfig; + @Output() scrolledToBottom = new EventEmitter(); + + LogDateFormatType = LogDateFormatType; + + hasDiff(log: AuditLogs): boolean { + return !!(log.data?.diff || log.data?.list?.diff); + } + + // User-related helpers + getUserFullName(user: User): string { + if (!user?.firstName && !user?.lastName) return ''; + return `${user.firstName || ''} ${user.lastName || ''}`.trim(); + } + + isSystemUser(user: User): boolean { + return user?.email === SYSTEM_USER_EMAIL; + } + + hasUserImage(user: User): boolean { + return !!user?.imageUrl; + } + + shouldTruncateUserName(user: User): boolean { + return this.getUserFullName(user).length >= 30; + } + + // Entity-specific helpers (delegated to config) + isSimpleLogType(type: string): boolean { + return this.config?.isSimpleLogType(type) ?? false; + } + + isStateChangeOrCreated(type: string): boolean { + return this.config?.isStateChangeOrCreated(type) ?? false; + } + + isUpdateLogType(type: string): boolean { + // Use custom function if provided, otherwise check if it's not simple or state change + if (this.config?.isUpdateLogType) { + return this.config.isUpdateLogType(type); + } + return !this.isSimpleLogType(type) && !this.isStateChangeOrCreated(type); + } + + hasListOperation(logData: AuditLogs[]): boolean { + return this.config?.hasListOperation(logData) ?? false; + } + + getListTableType(filterType: string): string { + return filterType === 'inclusion' ? 'include' : 'exclude'; + } + + getLogTypeMessage(type: string): string { + return this.config?.logTypeMessageMap?.[type] || ''; + } + + getListOperationMessage(operation: string): string { + return this.config?.listOperationMessageMap?.[operation] || ''; + } + + onScrolledToBottom(): void { + this.scrolledToBottom.emit(); + } +} diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/configs/experiment-timeline.config.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/configs/experiment-timeline.config.ts new file mode 100644 index 0000000000..670fca8225 --- /dev/null +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/configs/experiment-timeline.config.ts @@ -0,0 +1,43 @@ +import { AuditLogTimelineConfig } from '../common-audit-log-timeline-config.model'; +import { LOG_TYPE, EXPERIMENT_LIST_OPERATION } from 'upgrade_types'; + +/** + * Experiment-specific configuration for the audit log timeline component. + * Contains message maps and behavior functions for displaying experiment logs. + */ +export const EXPERIMENT_TIMELINE_LOG_TYPE_CONFIG: AuditLogTimelineConfig = { + logTypeMessageMap: { + [LOG_TYPE.EXPERIMENT_CREATED]: 'logs.audit-log-experiment-created.text', + [LOG_TYPE.EXPERIMENT_DELETED]: 'logs.audit-log-experiment-deleted.text', + [LOG_TYPE.EXPERIMENT_STATE_CHANGED]: 'logs.audit-log-experiment-state-changed.text', + [LOG_TYPE.EXPERIMENT_UPDATED]: 'logs.audit-log-experiment-updated.text', + [LOG_TYPE.EXPERIMENT_DATA_EXPORTED]: 'logs.audit-log-experiment-data-exported.text', + [LOG_TYPE.EXPERIMENT_DESIGN_EXPORTED]: 'logs.audit-log-experiment-design-exported.text', + }, + + listOperationMessageMap: { + [EXPERIMENT_LIST_OPERATION.CREATED]: 'logs.audit-log-list-created.text', + [EXPERIMENT_LIST_OPERATION.DELETED]: 'logs.audit-log-list-deleted.text', + [EXPERIMENT_LIST_OPERATION.UPDATED]: 'logs.audit-log-list-updated.text', + }, + + isSimpleLogType: (type: string): boolean => { + return [ + LOG_TYPE.EXPERIMENT_DELETED, + LOG_TYPE.EXPERIMENT_DATA_EXPORTED, + LOG_TYPE.EXPERIMENT_DESIGN_EXPORTED, + ].includes(type as LOG_TYPE); + }, + + isStateChangeOrCreated: (type: string): boolean => { + return type === LOG_TYPE.EXPERIMENT_STATE_CHANGED || type === LOG_TYPE.EXPERIMENT_CREATED; + }, + + hasListOperation: (logData: any): boolean => { + return !!logData?.list; + }, + + isUpdateLogType: (type: string): boolean => { + return type === LOG_TYPE.EXPERIMENT_UPDATED; + }, +}; diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/configs/feature-flag-timeline.config.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/configs/feature-flag-timeline.config.ts new file mode 100644 index 0000000000..3d6f4122a1 --- /dev/null +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-audit-log-timeline/configs/feature-flag-timeline.config.ts @@ -0,0 +1,47 @@ +import { AuditLogTimelineConfig } from '../common-audit-log-timeline-config.model'; +import { LOG_TYPE } from 'upgrade_types'; + +/** + * Feature flag-specific configuration for the audit log timeline component. + * + * NOTE: This is a placeholder configuration for when feature flag logs are implemented. + * To use this config, integrate it with a FeatureFlagLogSectionCardComponent that: + * 1. Fetches feature flag logs from the backend + * 2. Groups logs by date + * 3. Passes this config to CommonAuditLogTimelineComponent + */ +export const FEATURE_FLAG_TIMELINE_LOG_TYPE_CONFIG: AuditLogTimelineConfig = { + logTypeMessageMap: { + [LOG_TYPE.FEATURE_FLAG_CREATED]: 'logs.audit-log-feature-flag-created.text', + [LOG_TYPE.FEATURE_FLAG_DELETED]: 'logs.audit-log-feature-flag-deleted.text', + [LOG_TYPE.FEATURE_FLAG_STATUS_CHANGED]: 'logs.audit-log-feature-flag-state-changed.text', + [LOG_TYPE.FEATURE_FLAG_UPDATED]: 'logs.audit-log-feature-flag-updated.text', + [LOG_TYPE.FEATURE_FLAG_DATA_EXPORTED]: 'logs.audit-log-feature-flag-data-exported.text', + [LOG_TYPE.FEATURE_FLAG_DESIGN_EXPORTED]: 'logs.audit-log-feature-flag-design-exported.text', + }, + + // Feature flags use segment lists instead of experiment lists + // Adjust this if/when feature flag list operations are supported + listOperationMessageMap: undefined, + + isSimpleLogType: (type: string): boolean => { + return [ + LOG_TYPE.FEATURE_FLAG_DELETED, + LOG_TYPE.FEATURE_FLAG_DATA_EXPORTED, + LOG_TYPE.FEATURE_FLAG_DESIGN_EXPORTED, + ].includes(type as LOG_TYPE); + }, + + isStateChangeOrCreated: (type: string): boolean => { + return type === LOG_TYPE.FEATURE_FLAG_STATUS_CHANGED || type === LOG_TYPE.FEATURE_FLAG_CREATED; + }, + + hasListOperation: (logData: any): boolean => { + // Feature flags may have segment list operations - adjust based on actual data structure + return !!logData?.list; + }, + + isUpdateLogType: (type: string): boolean => { + return type === LOG_TYPE.FEATURE_FLAG_UPDATED; + }, +}; diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-section-card-search-header/common-section-card-search-header.component.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-section-card-search-header/common-section-card-search-header.component.ts index f4f5e2ffdc..e52b497f82 100644 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-section-card-search-header/common-section-card-search-header.component.ts +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-section-card-search-header/common-section-card-search-header.component.ts @@ -73,7 +73,7 @@ export interface FilterOption { changeDetection: ChangeDetectionStrategy.OnPush, }) export class CommonSectionCardSearchHeaderComponent implements OnChanges { - @Input() filterOptions: FilterOption[] = []; + @Input() dynamicFilterOptions: FilterOption[] = []; @Input() searchString: string; @Input() searchKey: string; @Output() search = new EventEmitter>(); @@ -82,8 +82,8 @@ export class CommonSectionCardSearchHeaderComponent implements OnChanges { groupedOptions: { groupName: string; options: FilterOption[] }[] = []; ngOnChanges(changes: SimpleChanges): void { - if (changes['filterOptions']) { - this.buidOptions(); + if (changes['dynamicFilterOptions']) { + this.rebuildOptions(); } } @@ -95,25 +95,25 @@ export class CommonSectionCardSearchHeaderComponent implements OnChanges { } get filterOptionsValues(): string[] { - return this.filterOptions.map((option) => option.value); + return this.dynamicFilterOptions.map((option) => option.value); } get filteredStatusOptions(): string[] { - return this.filterOptions.find((option) => option.value === this.searchKey)?.valueOptions || []; + return this.dynamicFilterOptions.find((option) => option.value === this.searchKey)?.valueOptions || []; } get isDropdown(): boolean { - return this.filterOptions.find((option) => option.value === this.searchKey)?.type === 'dropdown'; + return this.dynamicFilterOptions.find((option) => option.value === this.searchKey)?.type === 'dropdown'; } - private buidOptions(): void { + private rebuildOptions(): void { // Cache standalone options - this.standaloneOptions = this.filterOptions.filter((option) => !option.group && option.type !== 'group'); + this.standaloneOptions = this.dynamicFilterOptions.filter((option) => !option.group && option.type !== 'group'); // Cache grouped options const groups = new Map(); - this.filterOptions.forEach((option) => { + this.dynamicFilterOptions.forEach((option) => { if (option.group) { if (!groups.has(option.group)) { groups.set(option.group, []); diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/index.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/index.ts index 689ca15dd8..d7b00606d3 100644 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/index.ts +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/index.ts @@ -14,6 +14,8 @@ import { CommonTagComponent } from './common-tag/common-tag.component'; import { CommonTagListComponent } from './common-tag-list/common-tag-list.component'; import { CommonLearnMoreLinkComponent } from './common-learn-more-link/common-learn-more-link.component'; import { CommonSimpleTextValidatedConfirmationModalComponent } from './common-simple-text-validated-confirmation-modal/common-simple-text-validated-confirmation-modal.component'; +import { CommonAuditLogTimelineComponent } from './common-audit-log-timeline/common-audit-log-timeline.component'; +import { CommonAuditLogDiffDisplayComponent } from './common-audit-log-timeline/common-audit-log-diff-display/common-audit-log-diff-display.component'; export { CommonPageComponent, @@ -32,4 +34,6 @@ export { CommonTagListComponent, CommonLearnMoreLinkComponent, CommonSimpleTextValidatedConfirmationModalComponent, + CommonAuditLogTimelineComponent, + CommonAuditLogDiffDisplayComponent, }; From 30528aeef9ae0387b57acb75213297ef6e957b74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:23:04 +0000 Subject: [PATCH 2/2] Initial plan