Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/frontend/src/components/metric-evidence-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it } from "vitest";

import { withOwnTarget } from "@/components/metric-evidence-context";

function target(metricKey: string, label = metricKey) {
return {
selection: {
metric_key: metricKey,
entity: { type: "person" as const, id: "person-1" },
period: { from: "2026-07-01", to: "2026-07-31" },
filters: [],
display_dimensions: [],
},
label,
};
}

describe("withOwnTarget", () => {
const scope = [
target("git.commits"),
target("git.prs"),
target("wiki.edits"),
];

it("keeps the caller's own selection, not the scope's copy of it", () => {
const own = {
...target("git.prs", "Pull requests"),
selection: {
...target("git.prs").selection,
display_dimensions: ["repository"],
},
};
const out = withOwnTarget(scope, own);
expect(out[1]).toBe(own);
expect(out[1]?.selection.display_dimensions).toEqual(["repository"]);
});

it("leaves the scope's order alone", () => {
const out = withOwnTarget(scope, target("git.prs"));
expect(out.map((entry) => entry.selection.metric_key)).toEqual([
"git.commits",
"git.prs",
"wiki.edits",
]);
});

it("leads with a metric the scope does not carry", () => {
const own = target("ai.cost");
const out = withOwnTarget(scope, own);
expect(out).toHaveLength(4);
expect(out[0]).toBe(own);
});

it("returns just the caller when there is no scope", () => {
const own = target("git.commits");
expect(withOwnTarget([], own)).toEqual([own]);
});
});
35 changes: 34 additions & 1 deletion src/frontend/src/components/metric-evidence-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,47 @@ export interface EvidenceDialogState {
title?: string;
}

export interface EvidenceDialogOptions {
title?: string;
activeMetricKey?: string;
}

export interface EvidenceDialogContextValue {
openEvidence: (selection: MetricEvidenceSelection, label: string) => void;
openEvidenceTargets: (
targets: readonly EvidenceDialogTarget[],
title?: string
options?: EvidenceDialogOptions
) => void;
}

/**
* The metrics a surface can offer alongside the one being opened, so every
* entry lands in the same dialog with the same picker.
*/
export const EvidenceScopeContext = createContext<
readonly EvidenceDialogTarget[]
>([]);

export function useEvidenceScope(): readonly EvidenceDialogTarget[] {
return useContext(EvidenceScopeContext);
}

/**
* The scope with `own` in place of its own metric, so the selection the caller
* built — its filters and display dimensions — is the one that opens.
*/
export function withOwnTarget(
scope: readonly EvidenceDialogTarget[],
own: EvidenceDialogTarget
): EvidenceDialogTarget[] {
const key = own.selection.metric_key;
return scope.some((target) => target.selection.metric_key === key)
? scope.map((target) =>
target.selection.metric_key === key ? own : target
)
: [own, ...scope];
}

export const EvidenceDialogContext = createContext<
EvidenceDialogContextValue | undefined
>(undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function Controls() {
{ selection: git, label: "Duplicate" },
{ selection: wiki, label: "Wiki" },
],
"Combined"
{ title: "Combined" }
)
}
>
Expand All @@ -96,6 +96,30 @@ function Controls() {
<button type="button" onClick={() => evidence.openEvidenceTargets([])}>
open empty
</button>
<button
type="button"
onClick={() =>
evidence.openEvidenceTargets(
[
{ selection: git, label: "Commits" },
{ selection: wiki, label: "Wiki" },
],
{ activeMetricKey: wiki.metric_key }
)
}
>
open on wiki
</button>
<button
type="button"
onClick={() =>
evidence.openEvidenceTargets([{ selection: git, label: "Commits" }], {
activeMetricKey: "not.here",
})
}
>
open on a stranger
</button>
</>
);
}
Expand Down Expand Up @@ -159,4 +183,30 @@ describe("MetricEvidenceDialogProvider", () => {
queryKey: ["metric-drilldown"],
});
});

it("opens on the metric the caller asked for, not the first one", async () => {
const user = userEvent.setup();
render(
<MetricEvidenceDialogProvider>
<Controls />
</MetricEvidenceDialogProvider>
);

await user.click(screen.getByRole("button", { name: "open on wiki" }));
expect(screen.getByText("wiki.pages")).toBeInTheDocument();
});

it("falls back to the first metric when the requested one is absent", async () => {
const user = userEvent.setup();
render(
<MetricEvidenceDialogProvider>
<Controls />
</MetricEvidenceDialogProvider>
);

await user.click(
screen.getByRole("button", { name: "open on a stranger" })
);
expect(screen.getByText("git.commits")).toBeInTheDocument();
});
});
13 changes: 10 additions & 3 deletions src/frontend/src/components/metric-evidence-dialog-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { sessionAuthorizationScope } from "@/auth/session-scope";
import { useAuth } from "@/auth/use-auth";
import {
EvidenceDialogContext,
type EvidenceDialogOptions,
type EvidenceDialogState,
} from "@/components/metric-evidence-context";
import { MetricEvidenceDialog } from "@/components/metric-evidence-dialog";
Expand Down Expand Up @@ -41,7 +42,7 @@ export function MetricEvidenceDialogProvider({
const openEvidenceTargets = useCallback(
(
targets: readonly EvidenceDialogState["targets"][number][],
title?: EvidenceDialogState["title"]
options?: EvidenceDialogOptions
) => {
const uniqueTargets = [
...new Map(
Expand All @@ -50,10 +51,16 @@ export function MetricEvidenceDialogProvider({
];
const first = uniqueTargets[0];
if (!first) return;
const requested = options?.activeMetricKey;
const active = uniqueTargets.some(
(target) => target.selection.metric_key === requested
)
? requested!
: first.selection.metric_key;
setState({
targets: [first, ...uniqueTargets.slice(1)],
activeMetricKey: first.selection.metric_key,
title,
activeMetricKey: active,
title: options?.title,
sessionScope,
});
},
Expand Down
56 changes: 37 additions & 19 deletions src/frontend/src/components/widgets/dashboard/members-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ import { applyFocusStatus, STATUS_TEXT_CLASS } from "@/lib/status";
import { TEXT_FIGURE } from "@/lib/type-scale";
import { cn } from "@/lib/utils";
import { evidenceSelection } from "@/api/metric-drilldown-client";
import { useMetricEvidenceOptional } from "@/components/metric-evidence-context";
import {
EvidenceScopeContext,
useEvidenceScope,
useMetricEvidenceOptional,
withOwnTarget,
} from "@/components/metric-evidence-context";

export interface MembersGridMember {
/** Canonical person id — keys every metric lookup AND the IC link. */
Expand Down Expand Up @@ -479,6 +484,14 @@ function MemberRow({
: counts.top > 0
? "top"
: "in_pack";
const rowEvidenceTargets = cells.flatMap((cell) => {
if (!cell.col.metric.drilldown) return [];
const selection = evidenceSelection(
cell.col.metric.selection,
member.entityId
);
return selection ? [{ selection, label: cell.col.label }] : [];
});
return (
<tr>
<th
Expand Down Expand Up @@ -512,17 +525,19 @@ function MemberRow({
) : null}
</div>
</th>
{cells.map((cell) => (
<td key={cell.col.key} className="p-0 align-middle">
<GridCell
cell={cell}
entityId={member.entityId}
memberName={member.displayName}
cohortLabel={cohortLabel}
focusMode={focusMode}
/>
</td>
))}
<EvidenceScopeContext.Provider value={rowEvidenceTargets}>
{cells.map((cell) => (
<td key={cell.col.key} className="p-0 align-middle">
<GridCell
cell={cell}
entityId={member.entityId}
memberName={member.displayName}
cohortLabel={cohortLabel}
focusMode={focusMode}
/>
</td>
))}
</EvidenceScopeContext.Provider>
</tr>
);
}
Expand All @@ -542,6 +557,7 @@ function GridCell({
}) {
const focused = applyFocus(cell.status, focusMode);
const evidenceContext = useMetricEvidenceOptional();
const scope = useEvidenceScope();
const { col, value, previous, delta, median, observed } = cell;
const evidence = col.metric.drilldown
? evidenceSelection(
Expand Down Expand Up @@ -599,7 +615,14 @@ function GridCell({
<button
type="button"
onClick={() => {
if (evidence) evidenceContext?.openEvidence(evidence, col.label);
if (!evidence) return;
evidenceContext?.openEvidenceTargets(
withOwnTarget(scope, {
selection: evidence,
label: col.label,
}),
{ activeMetricKey: evidence.metric_key }
);
}}
aria-label={
observed
Expand Down Expand Up @@ -630,12 +653,7 @@ function GridCell({
<div className="flex flex-col gap-1">
<p className="text-sm font-semibold">{col.label}</p>
<p className="text-xs text-muted-foreground">{memberName}</p>
<p
className={cn(
"mt-2", TEXT_FIGURE, "",
PEER_TEXT[focused]
)}
>
<p className={cn("mt-2", TEXT_FIGURE, "", PEER_TEXT[focused])}>
{displayWithUnit}
</p>
<p className="text-xs text-muted-foreground">
Expand Down
Loading
Loading