From 286da5decf05432d527db5383163e950b20c55a8 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Tue, 3 Mar 2026 23:17:51 +0000 Subject: [PATCH 1/3] Add overview support and emitter display names to spec-dashboard - Add showOverview option and coverage overview component - Add emitterDisplayNames option for friendly column headers - Extract coverage utility functions - Refactor dashboard table to use display names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-dashboard-overview-2026-03-03-23-31-23.md | 7 + packages/spec-dashboard/src/apis.ts | 4 + .../src/components/coverage-overview.tsx | 182 ++++++++++++++++++ .../src/components/dashboard-az-storage.tsx | 2 + .../src/components/dashboard-table.tsx | 39 ++-- .../src/components/dashboard.tsx | 18 +- .../src/utils/coverage-utils.ts | 26 +++ 7 files changed, 255 insertions(+), 23 deletions(-) create mode 100644 .chronus/changes/spec-dashboard-overview-2026-03-03-23-31-23.md create mode 100644 packages/spec-dashboard/src/components/coverage-overview.tsx create mode 100644 packages/spec-dashboard/src/utils/coverage-utils.ts diff --git a/.chronus/changes/spec-dashboard-overview-2026-03-03-23-31-23.md b/.chronus/changes/spec-dashboard-overview-2026-03-03-23-31-23.md new file mode 100644 index 00000000000..a1d056382ee --- /dev/null +++ b/.chronus/changes/spec-dashboard-overview-2026-03-03-23-31-23.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/spec-dashboard" +--- + +Add coverage overview component and emitter display name support diff --git a/packages/spec-dashboard/src/apis.ts b/packages/spec-dashboard/src/apis.ts index e5b554b5c91..084b8d4d67a 100644 --- a/packages/spec-dashboard/src/apis.ts +++ b/packages/spec-dashboard/src/apis.ts @@ -30,6 +30,10 @@ export interface CoverageFromAzureStorageOptions { readonly tables?: TableDefinition[]; /** Optional tier config to filter scenarios by tier */ readonly tiers?: TierConfig; + /** Show coverage overview cards at the top of the dashboard */ + readonly showOverview?: boolean; + /** Optional friendly display names for emitters. Key is the emitter package name, value is the display name. */ + readonly emitterDisplayNames?: Readonly>; } export interface GeneratorCoverageSuiteReport extends CoverageReport { diff --git a/packages/spec-dashboard/src/components/coverage-overview.tsx b/packages/spec-dashboard/src/components/coverage-overview.tsx new file mode 100644 index 00000000000..cdcd6b5de75 --- /dev/null +++ b/packages/spec-dashboard/src/components/coverage-overview.tsx @@ -0,0 +1,182 @@ +import { Card, Text, tokens } from "@fluentui/react-components"; +import { FunctionComponent, useMemo } from "react"; +import { CoverageSummary } from "../apis.js"; +import { GroupRatioColors, GroupRatios } from "../constants.js"; + +interface EmitterOverview { + name: string; + displayName: string; + coverageRatio: number; +} + +export interface CoverageOverviewProps { + coverageSummaries: CoverageSummary[]; + emitterDisplayNames?: Record; +} + +/** + * Extracts a display-friendly name from a full emitter package name. + * e.g. "@typespec/http-client-python" → "Python" + */ +function getEmitterDisplayName( + emitterName: string, + report: CoverageSummary["generatorReports"][string], + emitterDisplayNames?: Record, +): string { + if (emitterDisplayNames?.[emitterName]) { + return emitterDisplayNames[emitterName]; + } + if (report?.generatorMetadata?.name) { + return report.generatorMetadata.name; + } + // Strip common prefix patterns + const match = emitterName.match(/http-client-(\w+)$/); + if (match) { + return match[1].charAt(0).toUpperCase() + match[1].slice(1); + } + return emitterName; +} + +/** + * Gets the accent color for a coverage ratio using the same thresholds as the coverage tables. + */ +function getOverviewColor(ratio: number): string { + for (const [key, threshold] of Object.entries(GroupRatios)) { + if (ratio >= threshold) { + return GroupRatioColors[key as keyof typeof GroupRatios]; + } + } + return GroupRatioColors.zero; +} + +/** + * Displays a section with a grid of cards showing per-emitter coverage overview. + */ +export const CoverageOverview: FunctionComponent = ({ + coverageSummaries, + emitterDisplayNames, +}) => { + const emitterOverviews = useMemo(() => { + // Aggregate scenarios per emitter across all summaries + const emitterMap = new Map< + string, + { + totalScenarios: number; + coveredScenarios: number; + report: CoverageSummary["generatorReports"][string]; + } + >(); + + for (const summary of coverageSummaries) { + for (const [emitterName, report] of Object.entries(summary.generatorReports)) { + if (!emitterMap.has(emitterName)) { + emitterMap.set(emitterName, { totalScenarios: 0, coveredScenarios: 0, report }); + } + const entry = emitterMap.get(emitterName)!; + const scenarios = summary.manifest.scenarios; + entry.totalScenarios += scenarios.length; + if (report) { + for (const scenario of scenarios) { + const status = report.results[scenario.name]; + if (status === "pass" || status === "not-applicable" || status === "not-supported") { + entry.coveredScenarios++; + } + } + } + } + } + + const overviews: EmitterOverview[] = []; + for (const [emitterName, data] of emitterMap) { + overviews.push({ + name: emitterName, + displayName: getEmitterDisplayName(emitterName, data.report, emitterDisplayNames), + coverageRatio: data.totalScenarios > 0 ? data.coveredScenarios / data.totalScenarios : 0, + }); + } + + return overviews; + }, [coverageSummaries, emitterDisplayNames]); + + if (emitterOverviews.length === 0) { + return null; + } + + return ( +
+ + Coverage Overview + +
+ {emitterOverviews.map((emitter) => ( + + ))} +
+
+ ); +}; + +interface EmitterOverviewCardProps { + emitter: EmitterOverview; +} + +const EmitterOverviewCard: FunctionComponent = ({ emitter }) => { + const accentColor = getOverviewColor(emitter.coverageRatio); + const percentage = Math.floor(emitter.coverageRatio * 100); + + return ( + + + {emitter.displayName} + + + {percentage}% + + + ); +}; diff --git a/packages/spec-dashboard/src/components/dashboard-az-storage.tsx b/packages/spec-dashboard/src/components/dashboard-az-storage.tsx index 671b4e7c69e..a3a9a89aeca 100644 --- a/packages/spec-dashboard/src/components/dashboard-az-storage.tsx +++ b/packages/spec-dashboard/src/components/dashboard-az-storage.tsx @@ -25,6 +25,8 @@ export const DashboardFromAzureStorage = (props: DashboardFromAzureStorageProps) ) : ( "Loading" diff --git a/packages/spec-dashboard/src/components/dashboard-table.tsx b/packages/spec-dashboard/src/components/dashboard-table.tsx index 45ef15a6341..2effe6f9e57 100644 --- a/packages/spec-dashboard/src/components/dashboard-table.tsx +++ b/packages/spec-dashboard/src/components/dashboard-table.tsx @@ -1,10 +1,11 @@ import { css } from "@emotion/react"; import { Popover, PopoverSurface, PopoverTrigger, tokens } from "@fluentui/react-components"; import { CodeBlock16Filled, Print16Filled } from "@fluentui/react-icons"; -import { ScenarioData, ScenarioManifest } from "@typespec/spec-coverage-sdk"; +import { ScenarioManifest } from "@typespec/spec-coverage-sdk"; import { FunctionComponent, useCallback, useMemo, useState } from "react"; import { CoverageSummary, GeneratorCoverageSuiteReport } from "../apis.js"; import { Colors } from "../constants.js"; +import { getCompletedRatio } from "../utils/coverage-utils.js"; import { GeneratorInformation } from "./generator-information.js"; import { ScenarioGroupRatioStatusBox } from "./scenario-group-status.js"; import { ScenarioStatusBox } from "./scenario-status.js"; @@ -13,6 +14,7 @@ import { ManifestTreeNode, TreeTableRow } from "./tree-table/types.js"; export interface DashboardTableProps { coverageSummary: CoverageSummary; + emitterDisplayNames?: Record; } function buildTreeRows( @@ -52,7 +54,10 @@ function buildTreeRows( return rows; } -export const DashboardTable: FunctionComponent = ({ coverageSummary }) => { +export const DashboardTable: FunctionComponent = ({ + coverageSummary, + emitterDisplayNames, +}) => { const languages: string[] = Object.keys(coverageSummary.generatorReports) as any; const tree = useMemo(() => createTree(coverageSummary.manifest), [coverageSummary.manifest]); @@ -78,7 +83,7 @@ export const DashboardTable: FunctionComponent = ({ coverag return ( - + {rows}
@@ -134,28 +139,16 @@ const ScenarioGroupStatusBox: FunctionComponent = ( return ; }; -function getCompletedRatio( - scenarios: ScenarioData[], - report: GeneratorCoverageSuiteReport, - scope: string = "", -) { - const filtered = scenarios.filter((x) => x.name.startsWith(scope)); - let coveredCount = 0; - for (const scenario of filtered) { - const status = report.results[scenario.name]; - if (status === "pass" || status === "not-applicable" || status === "not-supported") { - coveredCount++; - } - } - - return coveredCount / filtered.length; -} interface DashboardHeaderRowProps { coverageSummary: CoverageSummary; + emitterDisplayNames?: Record; } -const DashboardHeaderRow: FunctionComponent = ({ coverageSummary }) => { +const DashboardHeaderRow: FunctionComponent = ({ + coverageSummary, + emitterDisplayNames, +}) => { const data: [string, number, GeneratorCoverageSuiteReport | undefined][] = Object.entries( coverageSummary.generatorReports, ).map(([language, report]) => { @@ -171,7 +164,7 @@ const DashboardHeaderRow: FunctionComponent = ({ covera {tableHeader} {data.map(([lang, status, report]) => ( - + ))} ); @@ -200,12 +193,14 @@ export interface GeneratorHeaderCellProps { status: number; report: GeneratorCoverageSuiteReport | undefined; language: string; + displayName?: string; } export const GeneratorHeaderCell: FunctionComponent = ({ status, report, language, + displayName, }) => { return ( @@ -238,7 +233,7 @@ export const GeneratorHeaderCell: FunctionComponent = > -
{report?.generatorMetadata?.name ?? language}
+
{displayName ?? report?.generatorMetadata?.name ?? language}
{report && } diff --git a/packages/spec-dashboard/src/components/dashboard.tsx b/packages/spec-dashboard/src/components/dashboard.tsx index 7ab913f8924..b1daa51c938 100644 --- a/packages/spec-dashboard/src/components/dashboard.tsx +++ b/packages/spec-dashboard/src/components/dashboard.tsx @@ -3,6 +3,7 @@ import { FunctionComponent, useState } from "react"; import { CoverageSummary } from "../apis.js"; import { useTierFiltering } from "../hooks/use-tier-filtering.js"; import { TierConfig } from "../utils/tier-filtering-utils.js"; +import { CoverageOverview } from "./coverage-overview.js"; import { DashboardTable } from "./dashboard-table.js"; import { InfoEntry, InfoReport } from "./info-table.js"; import { TierFilterTabs } from "./tier-filter.js"; @@ -10,11 +11,17 @@ import { TierFilterTabs } from "./tier-filter.js"; export interface DashboardProps { coverageSummaries: CoverageSummary[]; scenarioTierConfig?: TierConfig; + /** Show coverage overview cards at the top of the dashboard */ + showOverview?: boolean; + /** Optional friendly display names for emitters. Key is the emitter package name. */ + emitterDisplayNames?: Record; } export const Dashboard: FunctionComponent = ({ coverageSummaries, scenarioTierConfig, + showOverview, + emitterDisplayNames, }) => { const [selectedTier, setSelectedTier] = useState(undefined); @@ -28,7 +35,10 @@ export const Dashboard: FunctionComponent = ({ .filter((s) => !selectedTier || s.manifest.scenarios.length > 0) .map((coverageSummary, i) => (
- +
)); @@ -45,6 +55,12 @@ export const Dashboard: FunctionComponent = ({ selectedTier={selectedTier} setSelectedTier={setSelectedTier} /> + {showOverview && ( + + )}
{specsCardTable}
{summaryTables} diff --git a/packages/spec-dashboard/src/utils/coverage-utils.ts b/packages/spec-dashboard/src/utils/coverage-utils.ts new file mode 100644 index 00000000000..9cb28cdfc5c --- /dev/null +++ b/packages/spec-dashboard/src/utils/coverage-utils.ts @@ -0,0 +1,26 @@ +import { ScenarioData } from "@typespec/spec-coverage-sdk"; +import { GeneratorCoverageSuiteReport } from "../apis.js"; + +/** + * Calculates the ratio of completed (pass, not-applicable, not-supported) scenarios. + * @param scenarios - All scenarios to consider + * @param report - The generator coverage report + * @param scope - Optional prefix to filter scenarios by + * @returns A ratio between 0 and 1 + */ +export function getCompletedRatio( + scenarios: ScenarioData[], + report: GeneratorCoverageSuiteReport, + scope: string = "", +): number { + const filtered = scenarios.filter((x) => x.name.startsWith(scope)); + let coveredCount = 0; + for (const scenario of filtered) { + const status = report.results[scenario.name]; + if (status === "pass" || status === "not-applicable" || status === "not-supported") { + coveredCount++; + } + } + + return filtered.length > 0 ? coveredCount / filtered.length : 0; +} From b417628552f314d507b033cf9e5218ca252ba80c Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Tue, 3 Mar 2026 23:35:21 +0000 Subject: [PATCH 2/3] Fix formatting and lint --- .../src/components/dashboard-table.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/spec-dashboard/src/components/dashboard-table.tsx b/packages/spec-dashboard/src/components/dashboard-table.tsx index 2effe6f9e57..8b5f16bc51b 100644 --- a/packages/spec-dashboard/src/components/dashboard-table.tsx +++ b/packages/spec-dashboard/src/components/dashboard-table.tsx @@ -83,7 +83,10 @@ export const DashboardTable: FunctionComponent = ({ return ( - + {rows}
@@ -139,7 +142,6 @@ const ScenarioGroupStatusBox: FunctionComponent = ( return ; }; - interface DashboardHeaderRowProps { coverageSummary: CoverageSummary; emitterDisplayNames?: Record; @@ -164,7 +166,13 @@ const DashboardHeaderRow: FunctionComponent = ({ {tableHeader} {data.map(([lang, status, report]) => ( - + ))} ); From 112f34739ef2e2387302b7e6e5718841c5550fb1 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Wed, 4 Mar 2026 20:34:55 +0000 Subject: [PATCH 3/3] Fix scenario name structure --- .../src/components/dashboard-table.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/spec-dashboard/src/components/dashboard-table.tsx b/packages/spec-dashboard/src/components/dashboard-table.tsx index 8b5f16bc51b..fc06aef8bdc 100644 --- a/packages/spec-dashboard/src/components/dashboard-table.tsx +++ b/packages/spec-dashboard/src/components/dashboard-table.tsx @@ -323,18 +323,5 @@ function createTree(manifest: ScenarioManifest): ManifestTreeNode { current.scenario = scenario; } - return cutTillMultipleChildren(root); -} - -function cutTillMultipleChildren(node: ManifestTreeNode): ManifestTreeNode { - let newRoot: ManifestTreeNode = node; - while (newRoot.children) { - if (Object.keys(newRoot.children).length === 1) { - newRoot = Object.values(newRoot.children)[0]; - } else { - break; - } - } - - return newRoot; + return root; }