Skip to content

Commit 44b214f

Browse files
authored
Implement experiment metrics table component (#2622)
* Implement experiment metrics table component * Fix the statistic order for categorical metrics * Update table row styles
1 parent 39502e0 commit 44b214f

File tree

6 files changed

+374
-13
lines changed

6 files changed

+374
-13
lines changed

frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-metrics-section-card/experiment-metrics-section-card.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,9 @@
2525
<app-experiment-metrics-table
2626
content
2727
*ngIf="isSectionCardExpanded"
28+
[queries]="experiment.queries || []"
29+
[isLoading$]="isLoadingExperiment$"
30+
[actionsDisabled]="!(permissions$ | async)?.experiments.update"
31+
(rowAction)="onRowAction($event, experiment.id)"
2832
></app-experiment-metrics-table>
2933
</app-common-section-card>

frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-metrics-section-card/experiment-metrics-section-card.component.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ import {
77
import { CommonModule } from '@angular/common';
88
import { TranslateModule } from '@ngx-translate/core';
99
import { IMenuButtonItem } from 'upgrade_types';
10-
import { ExperimentMetricsTableComponent } from './experiment-metrics-table/experiment-metrics-table.component';
10+
import {
11+
ExperimentMetricsTableComponent,
12+
ExperimentQueryRowActionEvent,
13+
} from './experiment-metrics-table/experiment-metrics-table.component';
1114
import { ExperimentService } from '../../../../../../../core/experiments/experiments.service';
12-
import { Observable } from 'rxjs';
13-
import { Experiment, EXPERIMENT_BUTTON_ACTION } from '../../../../../../../core/experiments/store/experiments.model';
15+
import { Observable, map } from 'rxjs';
16+
import {
17+
Experiment,
18+
EXPERIMENT_BUTTON_ACTION,
19+
EXPERIMENT_ROW_ACTION,
20+
} from '../../../../../../../core/experiments/store/experiments.model';
1421
import { UserPermission } from '../../../../../../../core/auth/store/auth.models';
1522
import { AuthService } from '../../../../../../../core/auth/auth.service';
1623

@@ -33,9 +40,15 @@ export class ExperimentMetricsSectionCardComponent implements OnInit {
3340

3441
permissions$: Observable<UserPermission>;
3542
selectedExperiment$ = this.experimentService.selectedExperiment$;
43+
isLoadingExperiment$ = this.experimentService.isLoadingExperiment$;
3644

37-
// TODO: Add tableRowCount$ when experiment metrics are implemented
38-
tableRowCount = 0;
45+
tableRowCount$ = this.selectedExperiment$.pipe(map((experiment) => experiment?.queries?.length || 0));
46+
47+
get tableRowCount(): number {
48+
let count = 0;
49+
this.tableRowCount$.subscribe((val) => (count = val));
50+
return count;
51+
}
3952

4053
menuButtonItems: IMenuButtonItem[] = [
4154
{
@@ -80,8 +93,26 @@ export class ExperimentMetricsSectionCardComponent implements OnInit {
8093
this.isSectionCardExpanded = isSectionCardExpanded;
8194
}
8295

83-
// TODO: Add row action methods when experiment metrics table events are implemented
84-
// onRowAction(event: ExperimentMetricRowActionEvent, experimentId: string): void {}
85-
// onEditMetric(rowData: ExperimentMetricTableRow, experimentId: string): void {}
86-
// onDeleteMetric(metric: ExperimentMetric): void {}
96+
onRowAction(event: ExperimentQueryRowActionEvent, experimentId: string): void {
97+
switch (event.action) {
98+
case EXPERIMENT_ROW_ACTION.EDIT:
99+
this.onEditMetric(event.query, experimentId);
100+
break;
101+
case EXPERIMENT_ROW_ACTION.DELETE:
102+
this.onDeleteMetric(event.query, experimentId);
103+
break;
104+
default:
105+
console.log('Unknown action:', event.action);
106+
}
107+
}
108+
109+
private onEditMetric(query: any, experimentId: string): void {
110+
// TODO: Implement edit metric functionality when dialog service is available
111+
console.log('Edit metric clicked for query:', query.id, 'in experiment:', experimentId);
112+
}
113+
114+
private onDeleteMetric(query: any, experimentId: string): void {
115+
// TODO: Implement delete metric functionality when dialog service is available
116+
console.log('Delete metric clicked for query:', query.id, 'in experiment:', experimentId);
117+
}
87118
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,93 @@
11
<div class="table-container">
2-
<p>experiment-metrics-table works!</p>
2+
<!-- Loading spinner -->
3+
<mat-progress-bar class="spinner" mode="indeterminate" *ngIf="isLoading$ | async"></mat-progress-bar>
4+
5+
<!-- Table -->
6+
<table
7+
mat-table
8+
[dataSource]="queries"
9+
[ngClass]="{ 'no-data': !queries?.length }"
10+
class="metrics-table"
11+
>
12+
<!-- Metric Column -->
13+
<ng-container matColumnDef="metric">
14+
<th mat-header-cell *matHeaderCellDef class="metric-column ft-14-600">
15+
{{ METRIC_TRANSLATION_KEYS.METRIC | translate }}
16+
</th>
17+
<td mat-cell *matCellDef="let row" class="metric-column ft-14-400">
18+
<div class="multi-line-cell" *ngIf="getMetricKeys(row).length > 0">
19+
<div *ngFor="let key of getMetricKeys(row)" class="metric-key">
20+
{{ key }}
21+
</div>
22+
</div>
23+
</td>
24+
</ng-container>
25+
26+
<!-- Statistic Column -->
27+
<ng-container matColumnDef="statistic">
28+
<th mat-header-cell *matHeaderCellDef class="statistic-column ft-14-600">
29+
{{ METRIC_TRANSLATION_KEYS.STATISTIC | translate }}
30+
</th>
31+
<td mat-cell *matCellDef="let row" class="statistic-column ft-14-400">
32+
<div class="multi-line-cell" *ngIf="getStatisticOperations(row).length > 0">
33+
<div *ngFor="let operation of getStatisticOperations(row)" class="statistic-operation">
34+
{{ operation | titlecase }}
35+
</div>
36+
</div>
37+
</td>
38+
</ng-container>
39+
40+
<!-- Display Name Column -->
41+
<ng-container matColumnDef="displayName">
42+
<th mat-header-cell *matHeaderCellDef class="display-name-column ft-14-600">
43+
{{ METRIC_TRANSLATION_KEYS.DISPLAY_NAME | translate }}
44+
</th>
45+
<td mat-cell *matCellDef="let row" class="display-name-column ft-14-400">
46+
<span *ngIf="row.name && row.name?.length <= 50">
47+
{{ row.name }}
48+
</span>
49+
<span *ngIf="row.name && row.name?.length > 50" [matTooltip]="row.name" matTooltipPosition="above">
50+
{{ row.name | truncate : 50 }}
51+
</span>
52+
</td>
53+
</ng-container>
54+
55+
<!-- Actions Column -->
56+
<ng-container matColumnDef="actions">
57+
<th mat-header-cell *matHeaderCellDef class="actions-column ft-14-600">
58+
{{ METRIC_TRANSLATION_KEYS.ACTIONS | translate }}
59+
</th>
60+
<td mat-cell *matCellDef="let row" class="actions-column ft-14-400 dense-2">
61+
<div class="button-wrapper">
62+
<button
63+
mat-icon-button
64+
class="action-button"
65+
[disabled]="actionsDisabled"
66+
(click)="onEditButtonClick(row)">
67+
<mat-icon>edit</mat-icon>
68+
</button>
69+
</div>
70+
<div class="button-wrapper">
71+
<button
72+
mat-icon-button
73+
class="action-button"
74+
[disabled]="actionsDisabled"
75+
(click)="onDeleteButtonClick(row)">
76+
<mat-icon>delete_outline</mat-icon>
77+
</button>
78+
</div>
79+
</td>
80+
</ng-container>
81+
82+
<!-- Header and Row Definitions -->
83+
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
84+
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
85+
86+
<!-- No Data Row -->
87+
<tr *matNoDataRow>
88+
<td class="ft-14-400" [attr.colspan]="displayedColumns.length">
89+
{{ 'experiments.details.metrics.card.no-data-row.text' | translate }}
90+
</td>
91+
</tr>
92+
</table>
393
</div>
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,121 @@
11
.table-container {
2+
position: relative;
3+
overflow: auto;
4+
width: 100%;
25
padding: 32px;
6+
7+
.spinner {
8+
position: sticky;
9+
top: 0;
10+
z-index: 1111;
11+
}
12+
13+
/* Add gap between header and body when no data exists */
14+
::ng-deep .no-data tbody:before {
15+
display: block;
16+
line-height: 8px;
17+
content: '\200C';
18+
}
19+
20+
.metrics-table {
21+
::ng-deep thead {
22+
background-color: var(--zircon);
23+
24+
tr.mat-mdc-header-row {
25+
height: 48px;
26+
border: 0;
27+
28+
th {
29+
padding-left: 0;
30+
color: var(--darker-grey);
31+
32+
&:first-child {
33+
border-top-left-radius: 4px;
34+
}
35+
36+
&:last-child {
37+
border-top-right-radius: 4px;
38+
}
39+
}
40+
}
41+
}
42+
43+
::ng-deep tbody {
44+
tr.mat-mdc-row {
45+
height: auto;
46+
min-height: 56px;
47+
48+
td {
49+
min-width: 96px;
50+
padding-left: 0;
51+
padding-top: 16px;
52+
padding-bottom: 8px;
53+
color: var(--black-2);
54+
vertical-align: top;
55+
}
56+
}
57+
58+
tr.mat-mdc-no-data-row {
59+
td {
60+
height: 48px;
61+
text-align: center;
62+
border: 1.5px dashed var(--light-grey-2);
63+
color: var(--dark-grey);
64+
}
65+
}
66+
}
67+
68+
.metric-column {
69+
width: 25%;
70+
padding-left: 32px;
71+
72+
.multi-line-cell {
73+
display: flex;
74+
flex-direction: column;
75+
gap: 4px;
76+
}
77+
}
78+
79+
.statistic-column {
80+
width: 25%;
81+
82+
.multi-line-cell {
83+
display: flex;
84+
flex-direction: column;
85+
gap: 4px;
86+
}
87+
}
88+
89+
.display-name-column {
90+
width: 40%;
91+
92+
span {
93+
word-break: break-word;
94+
}
95+
}
96+
97+
.actions-column {
98+
width: 10%;
99+
min-width: 96px;
100+
text-align: center;
101+
padding-right: 16px;
102+
padding-top: 8px;
103+
104+
.button-wrapper {
105+
display: inline-block;
106+
.action-button {
107+
color: var(--dark-grey);
108+
109+
&[disabled] {
110+
.mat-icon {
111+
opacity: 0.5;
112+
}
113+
}
114+
}
115+
&:has(.action-button:disabled) {
116+
cursor: not-allowed;
117+
}
118+
}
119+
}
120+
}
3121
}

0 commit comments

Comments
 (0)