From 87eb5dff71620532863d5ed2a824c47f24bad01d Mon Sep 17 00:00:00 2001 From: Hendrik De Vloed Date: Mon, 16 Feb 2026 11:14:00 +0100 Subject: [PATCH 1/4] Properly render and shorten generic class names Refactor type name display by introducing a new getName function in typeName.ts that shortens fully-qualified .NET type names, including generics and nested generics, to their simple names. --- .../src/components/grain-method-table.tsx | 3 +- .../src/components/grain-table.tsx | 5 +- .../src/grains/grain-details.tsx | 3 +- .../src/grains/grain.tsx | 6 +- .../Orleans.Dashboard.App/src/lib/typeName.ts | 74 +++++++++++++++++++ 5 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts diff --git a/src/Dashboard/Orleans.Dashboard.App/src/components/grain-method-table.tsx b/src/Dashboard/Orleans.Dashboard.App/src/components/grain-method-table.tsx index c4a8e9b37cf..af506abc8d8 100644 --- a/src/Dashboard/Orleans.Dashboard.App/src/components/grain-method-table.tsx +++ b/src/Dashboard/Orleans.Dashboard.App/src/components/grain-method-table.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { getName } from '../lib/typeName'; interface GrainMethodValue { grain: string; @@ -27,7 +28,7 @@ export default class GrainMethodTable extends React.Component - {value.grain} + {getName(value.grain)} diff --git a/src/Dashboard/Orleans.Dashboard.App/src/components/grain-table.tsx b/src/Dashboard/Orleans.Dashboard.App/src/components/grain-table.tsx index f117a3b6c2e..982d19aa8b5 100644 --- a/src/Dashboard/Orleans.Dashboard.App/src/components/grain-table.tsx +++ b/src/Dashboard/Orleans.Dashboard.App/src/components/grain-table.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { getName } from '../lib/typeName'; interface GrainStat { grainType: string; @@ -83,8 +84,8 @@ export default class GrainTable extends React.Component { - const parts = stat.grainType.split('.'); - const grainClassName = parts[parts.length - 1]; + // shorten fully-qualified type names, including generics + const grainClassName = getName(stat.grainType); const systemGrain = stat.grainType.startsWith('Orleans.'); const dashboardGrain = stat.grainType.startsWith('OrleansDashboard.'); return ( diff --git a/src/Dashboard/Orleans.Dashboard.App/src/grains/grain-details.tsx b/src/Dashboard/Orleans.Dashboard.App/src/grains/grain-details.tsx index a6ed9fe5660..2eea68511b4 100644 --- a/src/Dashboard/Orleans.Dashboard.App/src/grains/grain-details.tsx +++ b/src/Dashboard/Orleans.Dashboard.App/src/grains/grain-details.tsx @@ -3,6 +3,7 @@ import Page from '../components/page'; import http from '../lib/http'; import DisplayGrainState from '../components/display-grain-state'; import Panel from '../components/panel'; +import { getName } from '../lib/typeName'; interface GrainDetailsProps { grainTypes: string[]; @@ -68,7 +69,7 @@ export default class GrainDetails extends React.Component { - this.props.grainTypes.map((_item) => ) + this.props.grainTypes.map((_item) => ) } diff --git a/src/Dashboard/Orleans.Dashboard.App/src/grains/grain.tsx b/src/Dashboard/Orleans.Dashboard.App/src/grains/grain.tsx index 1ea090014b6..945661dcf0f 100644 --- a/src/Dashboard/Orleans.Dashboard.App/src/grains/grain.tsx +++ b/src/Dashboard/Orleans.Dashboard.App/src/grains/grain.tsx @@ -4,6 +4,7 @@ import CounterWidget from '../components/counter-widget'; import SiloBreakdown from './silo-table'; import Panel from '../components/panel'; import Page from '../components/page'; +import { getName } from '../lib/typeName'; interface GrainMethodValue { count: number; @@ -195,8 +196,3 @@ export default class Grain extends React.Component { return this.renderGraphs(); } } - -function getName(value: string): string { - const parts = value.split('.'); - return parts[parts.length - 1]; -} diff --git a/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts b/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts new file mode 100644 index 00000000000..73a335f64ed --- /dev/null +++ b/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts @@ -0,0 +1,74 @@ +export function getName(value: string): string { + // Parse a type name and shorten fully-qualified names to their last segment. + // Handles generic type arguments (angle brackets or square brackets) and + // shortens their content recursively. Examples: + // "A.B.C" -> "C" + // "G>" -> "G>" + function trimIdentifier(id: string): string { + if (!id) return id; + id = id.trim(); + // Remove assembly details if present: "Type, Assembly" + const comma = id.indexOf(','); + if (comma !== -1) id = id.substring(0, comma).trim(); + const parts = id.split('.'); + return parts[parts.length - 1]; + } + + function parseType(str: string): string { + if (!str) return str; + let s = str.trim(); + + // Find first generic opener: '<' or '[' + const lt = s.indexOf('<'); + const lb = s.indexOf('['); + let opener = ''; + let openerPos = -1; + let closer = ''; + if (lt !== -1 && (lb === -1 || lt < lb)) { + opener = '<'; closer = '>'; openerPos = lt; + } else if (lb !== -1) { + opener = '['; closer = ']'; openerPos = lb; + } + + if (openerPos === -1) { + return trimIdentifier(s); + } + + const main = s.substring(0, openerPos); + + // Find matching closer for the opener, honoring nested pairs + let depth = 0; + let end = -1; + for (let i = openerPos; i < s.length; i++) { + const ch = s[i]; + if (ch === opener) depth++; else if (ch === closer) { + depth--; if (depth === 0) { end = i; break; } + } + } + + if (end === -1) { + return trimIdentifier(s); + } + + const inner = s.substring(openerPos + 1, end); + + // Split inner by top-level commas only + const args: string[] = []; + let argStart = 0; + depth = 0; + for (let i = 0; i < inner.length; i++) { + const ch = inner[i]; + if (ch === '<' || ch === '[') depth++; else if (ch === '>' || ch === ']') depth--; + else if (ch === ',' && depth === 0) { + args.push(inner.substring(argStart, i)); + argStart = i + 1; + } + } + args.push(inner.substring(argStart)); + + const parsed = args.map(a => parseType(a)); + return `${trimIdentifier(main)}<${parsed.join(', ')}>`; + } + + return parseType(value); +} From b42b85831e5a387f51fede6a0cbc9b7604f24313 Mon Sep 17 00:00:00 2001 From: Hendrik De Vloed Date: Mon, 16 Feb 2026 13:11:17 +0100 Subject: [PATCH 2/4] Preserve method names Clicking through to a grain from the Grains section would show class names, not method names, on the individual call graphs. --- .../src/grains/grain.tsx | 24 ++++++++++++++- .../Orleans.Dashboard.App/src/lib/typeName.ts | 30 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/Dashboard/Orleans.Dashboard.App/src/grains/grain.tsx b/src/Dashboard/Orleans.Dashboard.App/src/grains/grain.tsx index 945661dcf0f..1d8c6cbf3e1 100644 --- a/src/Dashboard/Orleans.Dashboard.App/src/grains/grain.tsx +++ b/src/Dashboard/Orleans.Dashboard.App/src/grains/grain.tsx @@ -6,6 +6,28 @@ import Panel from '../components/panel'; import Page from '../components/page'; import { getName } from '../lib/typeName'; +// Format a fully-qualified member name so the type is shortened but the +// member/method part is preserved. Examples: +// - "A.B.C.M" -> "C.M" +// - "A.B.C.N.M" -> "C.N.M" +function formatMemberName(value: string): string { + if (!value) return value; + // find last top-level separator (dot, slash, or '#') not inside generics + let depth = 0; + for (let i = value.length - 1; i >= 0; i--) { + const ch = value[i]; + if (ch === '>' || ch === ']') depth++; + else if (ch === '<' || ch === '[') depth--; + else if (depth === 0 && (ch === '.' || ch === '/' || ch === '#')) { + const typePart = value.substring(0, i); + const memberPart = value.substring(i + 1); + return `${getName(typePart)}.${memberPart}`; + } + } + + return getName(value); +} + interface GrainMethodValue { count: number; elapsedTime: number; @@ -173,7 +195,7 @@ export default class Grain extends React.Component { .map(key => ( ))} diff --git a/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts b/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts index 73a335f64ed..a7737ba8816 100644 --- a/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts +++ b/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts @@ -51,6 +51,7 @@ export function getName(value: string): string { } const inner = s.substring(openerPos + 1, end); + const remainder = s.substring(end + 1); // Split inner by top-level commas only const args: string[] = []; @@ -67,7 +68,34 @@ export function getName(value: string): string { args.push(inner.substring(argStart)); const parsed = args.map(a => parseType(a)); - return `${trimIdentifier(main)}<${parsed.join(', ')}>`; + + // If there is a remainder after the generic (e.g. ".Method"), + // preserve it while shortening each dotted segment inside it. + function parseSuffix(rem: string): string { + if (!rem) return ''; + let i = 0; + // accept leading dots/spaces + while (i < rem.length && (rem[i] === '.' || rem[i] === ' ')) i++; + if (i === 0) return rem; // unexpected format, return raw + + const segments: string[] = []; + let segStart = i; + let depth = 0; + for (let j = i; j < rem.length; j++) { + const ch = rem[j]; + if (ch === '<' || ch === '[') depth++; else if (ch === '>' || ch === ']') depth--; + else if (ch === '.' && depth === 0) { + segments.push(rem.substring(segStart, j)); + segStart = j + 1; + } + } + if (segStart <= rem.length) segments.push(rem.substring(segStart)); + + const parsedSegs = segments.map(seg => parseType(seg)); + return '.' + parsedSegs.join('.'); + } + + return `${trimIdentifier(main)}<${parsed.join(', ')}>${parseSuffix(remainder)}`; } return parseType(value); From ce2226818741c186c9f803bc8cbe4522253a08c9 Mon Sep 17 00:00:00 2001 From: Hendrik De Vloed Date: Mon, 16 Feb 2026 18:06:19 +0100 Subject: [PATCH 3/4] Update src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts Also omit arity from generic class names. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts b/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts index a7737ba8816..e4aae64c177 100644 --- a/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts +++ b/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts @@ -11,7 +11,9 @@ export function getName(value: string): string { const comma = id.indexOf(','); if (comma !== -1) id = id.substring(0, comma).trim(); const parts = id.split('.'); - return parts[parts.length - 1]; + const last = parts[parts.length - 1]; + const tickIndex = last.indexOf('`'); + return tickIndex !== -1 ? last.substring(0, tickIndex) : last; } function parseType(str: string): string { From d8abdfcb06728f4ae82bbf4feadd33a83d0d3186 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Mon, 16 Feb 2026 15:02:11 -0800 Subject: [PATCH 4/4] Address dashboard generic-name PR feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/components/grain-method-table.tsx | 2 +- .../src/grains/grain-details.tsx | 2 +- .../Orleans.Dashboard.App/src/lib/typeName.ts | 200 ++++++++++-------- 3 files changed, 114 insertions(+), 90 deletions(-) diff --git a/src/Dashboard/Orleans.Dashboard.App/src/components/grain-method-table.tsx b/src/Dashboard/Orleans.Dashboard.App/src/components/grain-method-table.tsx index af506abc8d8..211346bfe40 100644 --- a/src/Dashboard/Orleans.Dashboard.App/src/components/grain-method-table.tsx +++ b/src/Dashboard/Orleans.Dashboard.App/src/components/grain-method-table.tsx @@ -28,7 +28,7 @@ export default class GrainMethodTable extends React.Component - {getName(value.grain)} + {getName(value.grain)} diff --git a/src/Dashboard/Orleans.Dashboard.App/src/grains/grain-details.tsx b/src/Dashboard/Orleans.Dashboard.App/src/grains/grain-details.tsx index 2eea68511b4..4f6fa201dd3 100644 --- a/src/Dashboard/Orleans.Dashboard.App/src/grains/grain-details.tsx +++ b/src/Dashboard/Orleans.Dashboard.App/src/grains/grain-details.tsx @@ -69,7 +69,7 @@ export default class GrainDetails extends React.Component { - this.props.grainTypes.map((_item) => ) + this.props.grainTypes.map((_item) => ) } diff --git a/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts b/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts index e4aae64c177..c2eae256de8 100644 --- a/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts +++ b/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts @@ -1,104 +1,128 @@ -export function getName(value: string): string { - // Parse a type name and shorten fully-qualified names to their last segment. - // Handles generic type arguments (angle brackets or square brackets) and - // shortens their content recursively. Examples: - // "A.B.C" -> "C" - // "G>" -> "G>" - function trimIdentifier(id: string): string { - if (!id) return id; - id = id.trim(); - // Remove assembly details if present: "Type, Assembly" - const comma = id.indexOf(','); - if (comma !== -1) id = id.substring(0, comma).trim(); - const parts = id.split('.'); - const last = parts[parts.length - 1]; - const tickIndex = last.indexOf('`'); - return tickIndex !== -1 ? last.substring(0, tickIndex) : last; +function trimIdentifier(id: string): string { + if (!id) return id; + const trimmed = stripAssemblyDetails(id.trim()); + const parts = trimmed.split('.'); + const last = parts[parts.length - 1]; + const tickIndex = last.indexOf('`'); + return tickIndex !== -1 ? last.substring(0, tickIndex) : last; +} + +function stripAssemblyDetails(value: string): string { + let depth = 0; + for (let i = 0; i < value.length; i++) { + const ch = value[i]; + if (ch === '<' || ch === '[') depth++; + else if (ch === '>' || ch === ']') depth--; + else if (ch === ',' && depth === 0) return value.substring(0, i).trim(); } - function parseType(str: string): string { - if (!str) return str; - let s = str.trim(); - - // Find first generic opener: '<' or '[' - const lt = s.indexOf('<'); - const lb = s.indexOf('['); - let opener = ''; - let openerPos = -1; - let closer = ''; - if (lt !== -1 && (lb === -1 || lt < lb)) { - opener = '<'; closer = '>'; openerPos = lt; - } else if (lb !== -1) { - opener = '['; closer = ']'; openerPos = lb; - } + return value.trim(); +} - if (openerPos === -1) { - return trimIdentifier(s); +function findMatchingCloser(value: string, start: number, opener: string, closer: string): number { + let depth = 0; + for (let i = start; i < value.length; i++) { + const ch = value[i]; + if (ch === opener) depth++; + else if (ch === closer) { + depth--; + if (depth === 0) return i; } + } + + return -1; +} - const main = s.substring(0, openerPos); +function splitTopLevelArguments(value: string): string[] { + const args: string[] = []; + let argStart = 0; + let depth = 0; - // Find matching closer for the opener, honoring nested pairs - let depth = 0; - let end = -1; - for (let i = openerPos; i < s.length; i++) { - const ch = s[i]; - if (ch === opener) depth++; else if (ch === closer) { - depth--; if (depth === 0) { end = i; break; } - } + for (let i = 0; i < value.length; i++) { + const ch = value[i]; + if (ch === '<' || ch === '[') depth++; + else if (ch === '>' || ch === ']') depth--; + else if (ch === ',' && depth === 0) { + args.push(value.substring(argStart, i)); + argStart = i + 1; } + } - if (end === -1) { - return trimIdentifier(s); - } + args.push(value.substring(argStart)); + return args; +} - const inner = s.substring(openerPos + 1, end); - const remainder = s.substring(end + 1); - - // Split inner by top-level commas only - const args: string[] = []; - let argStart = 0; - depth = 0; - for (let i = 0; i < inner.length; i++) { - const ch = inner[i]; - if (ch === '<' || ch === '[') depth++; else if (ch === '>' || ch === ']') depth--; - else if (ch === ',' && depth === 0) { - args.push(inner.substring(argStart, i)); - argStart = i + 1; - } +function parseSuffix(rem: string): string { + if (!rem) return ''; + let i = 0; + while (i < rem.length && (rem[i] === '.' || rem[i] === ' ')) i++; + if (i === 0) return rem; + + const segments: string[] = []; + let segStart = i; + let depth = 0; + for (let j = i; j < rem.length; j++) { + const ch = rem[j]; + if (ch === '<' || ch === '[') depth++; + else if (ch === '>' || ch === ']') depth--; + else if (ch === '.' && depth === 0) { + segments.push(rem.substring(segStart, j)); + segStart = j + 1; } - args.push(inner.substring(argStart)); - - const parsed = args.map(a => parseType(a)); - - // If there is a remainder after the generic (e.g. ".Method"), - // preserve it while shortening each dotted segment inside it. - function parseSuffix(rem: string): string { - if (!rem) return ''; - let i = 0; - // accept leading dots/spaces - while (i < rem.length && (rem[i] === '.' || rem[i] === ' ')) i++; - if (i === 0) return rem; // unexpected format, return raw - - const segments: string[] = []; - let segStart = i; - let depth = 0; - for (let j = i; j < rem.length; j++) { - const ch = rem[j]; - if (ch === '<' || ch === '[') depth++; else if (ch === '>' || ch === ']') depth--; - else if (ch === '.' && depth === 0) { - segments.push(rem.substring(segStart, j)); - segStart = j + 1; - } - } - if (segStart <= rem.length) segments.push(rem.substring(segStart)); - - const parsedSegs = segments.map(seg => parseType(seg)); - return '.' + parsedSegs.join('.'); + } + + if (segStart <= rem.length) segments.push(rem.substring(segStart)); + + return '.' + segments.map(seg => parseType(seg)).join('.'); +} + +function parseType(str: string): string { + if (!str) return str; + const s = stripAssemblyDetails(str.trim()); + if (!s) return s; + + // Handle assembly-qualified generic arguments wrapped in [ ... ]. + if (s[0] === '[') { + const wrappedEnd = findMatchingCloser(s, 0, '[', ']'); + if (wrappedEnd === s.length - 1) { + return parseType(stripAssemblyDetails(s.substring(1, wrappedEnd))); } + } + + // Find first generic opener: '<' or '[' + const lt = s.indexOf('<'); + const lb = s.indexOf('['); + let opener = ''; + let openerPos = -1; + let closer = ''; + if (lt !== -1 && (lb === -1 || lt < lb)) { + opener = '<'; + closer = '>'; + openerPos = lt; + } else if (lb !== -1) { + opener = '['; + closer = ']'; + openerPos = lb; + } - return `${trimIdentifier(main)}<${parsed.join(', ')}>${parseSuffix(remainder)}`; + if (openerPos === -1) { + return trimIdentifier(s); } + const main = s.substring(0, openerPos); + const end = findMatchingCloser(s, openerPos, opener, closer); + + if (end === -1) { + return trimIdentifier(s); + } + + const inner = s.substring(openerPos + 1, end); + const remainder = s.substring(end + 1); + const parsed = splitTopLevelArguments(inner).map(arg => parseType(arg.trim())); + + return `${trimIdentifier(main)}<${parsed.join(', ')}>${parseSuffix(remainder)}`; +} + +export function getName(value: string): string { return parseType(value); }