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);
+}