Skip to content

Commit 17fe1d5

Browse files
author
Dylan Huang
authored
Aggregated metrics part 9 (pivot table UI) (#64)
* add --port arg to ep logs * Fix WebSocketManager to reset broadcast task after cancellation * simple tests work * TODO: TestLogsServer * TODO: TestLogsServerIntegration * TODO: test HTML injection - also test TestAsyncWebSocketOperations * add logs server tests * add port parameter testes * use gpt-oss-120b to avoid rate limits * point to port 8000 for dev * woops * fix "uvicorn eval_protocol.utils.logs_server:create_app --factory --reload" * use gpt-oss-120b since less rate limiting (#57) * Aggregated metrics part 7 (#58) * use gpt-oss-120b for less rate limits and faster tests * fix typeerror * Refactor LogsServer event handling and improve integration tests - Moved event_bus.start_listening() to the correct location in LogsServer to ensure it starts listening during the broadcast loop. - Updated integration tests to use multiprocessing for server startup and improved health check validation. - Enhanced test_create_app_factory to be asynchronous and added necessary imports for better clarity. * Enhance test_create_app_factory to verify LogsServer start_loops call - Updated the test_create_app_factory to mock and assert that the start_loops method of LogsServer is called during app creation. - Ensured the test remains asynchronous and maintains clarity in its assertions. * fix * use active logger * cohort -> experiment * vite build * Update model path in pytest configuration to use gpt-oss-120b for improved performance * move ids under execution_metadata * move pivot tab into its own component * put flattened dataset into globalstate * parameterize pivottab * styling + add parameterization for aggregate method * add tests for min and max aggregate methods * add filter functionality * Enhance pivot functionality by adding support for column totals using various aggregation methods (avg, count) and updating tests to validate these changes. * styling * support date in flatten json * styling * better description * Update filter functionality to use 'contains' operator instead of '==' for improved filtering in PivotTab component. * styling * Implement global pivot configuration management in GlobalState and update PivotTab to utilize it, including loading, saving, and resetting functionality. * DRY things * consistently styling across inputs, button, select * make sure we show all keys from all rows * SearchableSelect * searchableselect * better styling * Remove unused Select import from PivotTab component * Add keyboard navigation and highlighting to SearchableSelect component * OR / AND filters * Enhance computePivot function to filter out records with undefined values in both row and column fields, and update tests to verify correct handling of such cases and row total calculations.
1 parent 6045ee9 commit 17fe1d5

16 files changed

Lines changed: 10689 additions & 106 deletions

vite-app/src/GlobalState.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,71 @@
11
import { makeAutoObservable } from "mobx";
22
import type { EvaluationRow } from "./types/eval-protocol";
3+
import type { PivotConfig } from "./types/filters";
4+
import flattenJson from "./util/flatten-json";
5+
6+
// Default pivot configuration
7+
const DEFAULT_PIVOT_CONFIG: PivotConfig = {
8+
selectedRowFields: ["$.eval_metadata.name"],
9+
selectedColumnFields: ["$.input_metadata.completion_params.model"],
10+
selectedValueField: "$.evaluation_result.score",
11+
selectedAggregator: "avg",
12+
filters: [],
13+
};
314

415
export class GlobalState {
516
isConnected: boolean = false;
617
// rollout_id -> EvaluationRow
718
dataset: Record<string, EvaluationRow> = {};
819
// rollout_id -> expanded
920
expandedRows: Record<string, boolean> = {};
21+
// Pivot configuration
22+
pivotConfig: PivotConfig;
1023

1124
constructor() {
25+
// Load pivot config from localStorage or use defaults
26+
this.pivotConfig = this.loadPivotConfig();
1227
makeAutoObservable(this);
1328
}
1429

30+
// Load pivot configuration from localStorage
31+
private loadPivotConfig(): PivotConfig {
32+
try {
33+
const stored = localStorage.getItem("pivotConfig");
34+
if (stored) {
35+
const parsed = JSON.parse(stored);
36+
// Merge with defaults to handle any missing properties
37+
return { ...DEFAULT_PIVOT_CONFIG, ...parsed };
38+
}
39+
} catch (error) {
40+
console.warn("Failed to load pivot config from localStorage:", error);
41+
}
42+
return { ...DEFAULT_PIVOT_CONFIG };
43+
}
44+
45+
// Save pivot configuration to localStorage
46+
private savePivotConfig() {
47+
try {
48+
localStorage.setItem("pivotConfig", JSON.stringify(this.pivotConfig));
49+
} catch (error) {
50+
console.warn("Failed to save pivot config to localStorage:", error);
51+
}
52+
}
53+
54+
// Update pivot configuration and save to localStorage
55+
updatePivotConfig(updates: Partial<PivotConfig>) {
56+
Object.assign(this.pivotConfig, updates);
57+
this.savePivotConfig();
58+
}
59+
60+
// Reset pivot configuration to defaults
61+
resetPivotConfig() {
62+
this.pivotConfig = {
63+
...DEFAULT_PIVOT_CONFIG,
64+
filters: [], // Ensure filters is an empty array of FilterGroups
65+
};
66+
this.savePivotConfig();
67+
}
68+
1569
upsertRows(dataset: EvaluationRow[]) {
1670
dataset.forEach((row) => {
1771
if (!row.execution_metadata?.rollout_id) {
@@ -53,6 +107,18 @@ export class GlobalState {
53107
);
54108
}
55109

110+
get flattenedDataset() {
111+
return this.sortedDataset.map((row) => flattenJson(row));
112+
}
113+
114+
get flattenedDatasetKeys() {
115+
const keySet = new Set<string>();
116+
this.flattenedDataset.forEach((row) => {
117+
Object.keys(row).forEach((key) => keySet.add(key));
118+
});
119+
return Array.from(keySet);
120+
}
121+
56122
get totalCount() {
57123
return Object.keys(this.dataset).length;
58124
}

vite-app/src/components/Button.tsx

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from "react";
2+
import { commonStyles } from "../styles/common";
23

34
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
45
variant?: "primary" | "secondary";
@@ -10,23 +11,11 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
1011
{ className = "", variant = "secondary", size = "sm", children, ...props },
1112
ref
1213
) => {
13-
const baseClasses = "border text-xs font-medium focus:outline-none";
14-
15-
const variantClasses = {
16-
primary: "border-gray-300 bg-gray-100 text-gray-700 hover:bg-gray-200",
17-
secondary: "border-gray-300 bg-gray-100 text-gray-700 hover:bg-gray-200",
18-
};
19-
20-
const sizeClasses = {
21-
sm: "px-2 py-0.5",
22-
md: "px-3 py-1",
23-
};
24-
2514
return (
2615
<button
2716
ref={ref}
28-
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
29-
style={{ boxShadow: "none" }}
17+
className={`${commonStyles.button.base} ${commonStyles.button.variant[variant]} ${commonStyles.button.size[size]} ${className}`}
18+
style={{ boxShadow: commonStyles.button.shadow }}
3019
{...props}
3120
>
3221
{children}

vite-app/src/components/Dashboard.tsx

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { observer } from "mobx-react";
2-
import { useMemo, useState, useEffect } from "react";
2+
import { useState, useEffect } from "react";
33
import { useLocation, useNavigate } from "react-router-dom";
44
import { state } from "../App";
55
import Button from "./Button";
66
import { EvaluationTable } from "./EvaluationTable";
7-
import PivotTable from "./PivotTable";
7+
import PivotTab from "./PivotTab";
88
import TabButton from "./TabButton";
9-
import flattenJson from "../util/flatten-json";
109

1110
interface DashboardProps {
1211
onRefresh: () => void;
@@ -68,11 +67,6 @@ const Dashboard = observer(({ onRefresh }: DashboardProps) => {
6867
setActiveTab(deriveTabFromPath(location.pathname));
6968
}, [location.pathname]);
7069

71-
const flattened = useMemo(() => {
72-
const flattenedDataset = state.sortedDataset.map((row) => flattenJson(row));
73-
return flattenedDataset;
74-
}, [state.sortedDataset]);
75-
7670
return (
7771
<div className="text-sm">
7872
{/* Summary */}
@@ -129,33 +123,7 @@ const Dashboard = observer(({ onRefresh }: DashboardProps) => {
129123

130124
{/* Tab content */}
131125
<div className="p-3">
132-
{activeTab === "table" ? (
133-
<EvaluationTable />
134-
) : (
135-
<div>
136-
<div className="text-xs text-gray-600 mb-2">
137-
Showing pivot of flattened rows (JSONPath keys). Defaults:
138-
rows by eval name and status; columns by model; values average
139-
score.
140-
</div>
141-
<PivotTable
142-
data={flattened}
143-
rowFields={[
144-
"$.eval_metadata.name" as keyof (typeof flattened)[number],
145-
"$.eval_metadata.status" as keyof (typeof flattened)[number],
146-
]}
147-
columnFields={[
148-
"$.input_metadata.completion_params.model" as keyof (typeof flattened)[number],
149-
]}
150-
valueField={
151-
"$.evaluation_result.score" as keyof (typeof flattened)[number]
152-
}
153-
aggregator="avg"
154-
showRowTotals
155-
showColumnTotals
156-
/>
157-
</div>
158-
)}
126+
{activeTab === "table" ? <EvaluationTable /> : <PivotTab />}
159127
</div>
160128
</div>
161129
)}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from "react";
2+
import type { FilterConfig } from "../types/filters";
3+
import { commonStyles } from "../styles/common";
4+
5+
interface FilterInputProps {
6+
filter: FilterConfig;
7+
onUpdate: (updates: Partial<FilterConfig>) => void;
8+
}
9+
10+
const FilterInput = ({ filter, onUpdate }: FilterInputProps) => {
11+
const fieldType = filter.type || "text";
12+
13+
if (fieldType === "date") {
14+
return (
15+
<div className="flex space-x-2">
16+
<input
17+
type="date"
18+
value={filter.value}
19+
onChange={(e) => onUpdate({ value: e.target.value })}
20+
className={`${commonStyles.input.base} ${commonStyles.input.size.sm} ${commonStyles.width.sm}`}
21+
style={{ boxShadow: commonStyles.input.shadow }}
22+
/>
23+
{filter.operator === "between" && (
24+
<input
25+
type="date"
26+
value={filter.value2 || ""}
27+
onChange={(e) => onUpdate({ value2: e.target.value })}
28+
className={`${commonStyles.input.base} ${commonStyles.input.size.sm} ${commonStyles.width.sm}`}
29+
placeholder="End date"
30+
style={{ boxShadow: commonStyles.input.shadow }}
31+
/>
32+
)}
33+
</div>
34+
);
35+
}
36+
37+
return (
38+
<input
39+
type="text"
40+
value={filter.value}
41+
onChange={(e) => onUpdate({ value: e.target.value })}
42+
placeholder="Value"
43+
className={`${commonStyles.input.base} ${commonStyles.input.size.sm} ${commonStyles.width.sm}`}
44+
style={{ boxShadow: commonStyles.input.shadow }}
45+
/>
46+
);
47+
};
48+
49+
export default FilterInput;

0 commit comments

Comments
 (0)