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..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 @@ -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..4f6fa201dd3 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..1d8c6cbf3e1 100644 --- a/src/Dashboard/Orleans.Dashboard.App/src/grains/grain.tsx +++ b/src/Dashboard/Orleans.Dashboard.App/src/grains/grain.tsx @@ -4,6 +4,29 @@ 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'; + +// 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; @@ -172,7 +195,7 @@ export default class Grain extends React.Component { .map(key => ( ))} @@ -195,8 +218,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..c2eae256de8 --- /dev/null +++ b/src/Dashboard/Orleans.Dashboard.App/src/lib/typeName.ts @@ -0,0 +1,128 @@ +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(); + } + + return value.trim(); +} + +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; +} + +function splitTopLevelArguments(value: string): string[] { + const args: string[] = []; + let argStart = 0; + 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) { + args.push(value.substring(argStart, i)); + argStart = i + 1; + } + } + + args.push(value.substring(argStart)); + return args; +} + +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; + } + } + + 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; + } + + 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); +}