diff --git a/kueue/locales/en/translation.json b/kueue/locales/en/translation.json
new file mode 100644
index 0000000000..17bfc8dd74
--- /dev/null
+++ b/kueue/locales/en/translation.json
@@ -0,0 +1,11 @@
+{
+ "Topologies": "Topologies",
+ "Kueue Topologies define node and hardware hierarchy levels for Topology-Aware Scheduling (TAS).": "Kueue Topologies define node and hardware hierarchy levels for Topology-Aware Scheduling (TAS).",
+ "Total Levels": "Total Levels",
+ "Levels Chain": "Levels Chain",
+ "Topology Levels Hierarchy": "Topology Levels Hierarchy",
+ "Unnamed Level": "Unnamed Level",
+ "Node Label": "Node Label",
+ "None": "None",
+ "Levels Hierarchy": "Levels Hierarchy"
+}
diff --git a/kueue/package.json b/kueue/package.json
index 0ca3f2cea1..2adcd4e46a 100644
--- a/kueue/package.json
+++ b/kueue/package.json
@@ -37,5 +37,8 @@
},
"overrides": {
"ws": "8.21.0"
+ },
+ "headlamp": {
+ "i18n": []
}
}
diff --git a/kueue/src/components/topologies/Detail.tsx b/kueue/src/components/topologies/Detail.tsx
new file mode 100644
index 0000000000..23c59b98de
--- /dev/null
+++ b/kueue/src/components/topologies/Detail.tsx
@@ -0,0 +1,80 @@
+import { useTranslation } from '@kinvolk/headlamp-plugin/lib';
+import { DetailsGrid } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
+import { SectionBox } from '@kinvolk/headlamp-plugin/lib/components/common';
+import { Box, Chip, Paper, Typography } from '@mui/material';
+import { useParams } from 'react-router-dom';
+import { Topology } from '../../resources/topology';
+import KueueAdminResourceAccess from '../common/KueueAdminResourceAccess';
+
+export default function TopologyDetail() {
+ const { t } = useTranslation();
+ const { name } = useParams<{ name: string }>();
+
+ return (
+
+
+ item
+ ? [
+ {
+ name: t('Total Levels'),
+ value: item.levelsCount,
+ },
+ {
+ name: t('Levels Chain'),
+ value: item.levelsDisplay,
+ },
+ ]
+ : []
+ }
+ extraSections={item =>
+ item && item.levels.length > 0
+ ? [
+
+
+ {item.levels.map((lvl, index) => (
+
+
+
+
+ {lvl.name || t('Unnamed Level')}
+
+
+
+
+ {t('Node Label')}:
+
+
+
+
+ ))}
+
+ ,
+ ]
+ : []
+ }
+ />
+
+ );
+}
diff --git a/kueue/src/components/topologies/List.tsx b/kueue/src/components/topologies/List.tsx
new file mode 100644
index 0000000000..9bbbe60c22
--- /dev/null
+++ b/kueue/src/components/topologies/List.tsx
@@ -0,0 +1,36 @@
+import { useTranslation } from '@kinvolk/headlamp-plugin/lib';
+import { ResourceListView } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
+import { Topology } from '../../resources/topology';
+import KueueAdminResourceAccess from '../common/KueueAdminResourceAccess';
+
+export default function TopologyList() {
+ const { t } = useTranslation();
+
+ return (
+
+ topology.levelsDisplay,
+ },
+ {
+ id: 'levelsCount',
+ label: t('Total Levels'),
+ getValue: (topology: Topology) => topology.levelsCount,
+ },
+ 'age',
+ ]}
+ />
+
+ );
+}
diff --git a/kueue/src/index.tsx b/kueue/src/index.tsx
index 58e82872af..2c84e4aa6b 100644
--- a/kueue/src/index.tsx
+++ b/kueue/src/index.tsx
@@ -5,6 +5,8 @@ import LocalQueueDetail from './components/localqueues/Detail';
import LocalQueueList from './components/localqueues/List';
import ResourceFlavorDetail from './components/resourceflavors/Detail';
import ResourceFlavorList from './components/resourceflavors/List';
+import TopologyDetail from './components/topologies/Detail';
+import TopologyList from './components/topologies/List';
import WorkloadDetail from './components/workloads/Detail';
import WorkloadList from './components/workloads/List';
import { kueueRouteNames, kueueRoutePaths } from './utils/kueueRoutes';
@@ -38,6 +40,13 @@ registerSidebarEntry({
url: kueueRoutePaths.resourceFlavorsList,
});
+registerSidebarEntry({
+ parent: 'kueue',
+ name: 'kueue-topologies',
+ label: 'Topologies',
+ url: kueueRoutePaths.topologiesList,
+});
+
registerSidebarEntry({
parent: 'kueue',
name: 'kueue-workloads',
@@ -93,6 +102,22 @@ registerRoute({
component: () => ,
});
+registerRoute({
+ path: kueueRoutePaths.topologiesList,
+ sidebar: 'kueue-topologies',
+ name: kueueRouteNames.topologiesList,
+ exact: true,
+ component: () => ,
+});
+
+registerRoute({
+ path: kueueRoutePaths.topologyDetail,
+ sidebar: 'kueue-topologies',
+ name: kueueRouteNames.topologyDetail,
+ exact: true,
+ component: () => ,
+});
+
registerRoute({
path: kueueRoutePaths.workloadsList,
sidebar: 'kueue-workloads',
diff --git a/kueue/src/resources/topology.test.ts b/kueue/src/resources/topology.test.ts
new file mode 100644
index 0000000000..a3a7536bbc
--- /dev/null
+++ b/kueue/src/resources/topology.test.ts
@@ -0,0 +1,84 @@
+import { describe, expect, it } from 'vitest';
+import {
+ getTopologyLevelNames,
+ renderTopologyLevel,
+ renderTopologyLevelsCount,
+ renderTopologyLevelsSummary,
+} from './topologyFormatters';
+
+describe('Topology formatters', () => {
+ describe('renderTopologyLevel', () => {
+ it('formats single topology level with node label', () => {
+ expect(
+ renderTopologyLevel({
+ name: 'rack',
+ nodeLabel: 'topology.kubernetes.io/rack',
+ })
+ ).toBe('rack (topology.kubernetes.io/rack)');
+ });
+
+ it('formats level without node label', () => {
+ expect(
+ renderTopologyLevel({
+ name: 'block',
+ nodeLabel: '',
+ })
+ ).toBe('block');
+ });
+
+ it('returns dash fallback for empty or missing level', () => {
+ expect(renderTopologyLevel(null)).toBe('-');
+ expect(renderTopologyLevel(undefined)).toBe('-');
+ expect(renderTopologyLevel({ name: '', nodeLabel: '' })).toBe('-');
+ });
+ });
+
+ describe('renderTopologyLevelsSummary', () => {
+ it('formats hierarchy chain with arrow separators', () => {
+ const levels = [
+ { name: 'rack', nodeLabel: 'topology.kubernetes.io/rack' },
+ { name: 'block', nodeLabel: 'topology.kubernetes.io/block' },
+ { name: 'host', nodeLabel: 'kubernetes.io/hostname' },
+ ];
+ expect(renderTopologyLevelsSummary(levels)).toBe(
+ 'rack (topology.kubernetes.io/rack) → block (topology.kubernetes.io/block) → host (kubernetes.io/hostname)'
+ );
+ });
+
+ it('returns dash fallback for empty or undefined levels array', () => {
+ expect(renderTopologyLevelsSummary([])).toBe('-');
+ expect(renderTopologyLevelsSummary(null)).toBe('-');
+ expect(renderTopologyLevelsSummary(undefined)).toBe('-');
+ });
+ });
+
+ describe('renderTopologyLevelsCount', () => {
+ it('returns total count of levels', () => {
+ expect(
+ renderTopologyLevelsCount([
+ { name: 'rack', nodeLabel: 'rack' },
+ { name: 'host', nodeLabel: 'host' },
+ ])
+ ).toBe(2);
+ expect(renderTopologyLevelsCount([])).toBe(0);
+ expect(renderTopologyLevelsCount(undefined)).toBe(0);
+ });
+ });
+
+ describe('getTopologyLevelNames', () => {
+ it('extracts list of non-empty level names', () => {
+ expect(
+ getTopologyLevelNames([
+ { name: 'rack', nodeLabel: '' },
+ { name: 'block', nodeLabel: '' },
+ { name: 'host', nodeLabel: '' },
+ ])
+ ).toEqual(['rack', 'block', 'host']);
+ });
+
+ it('returns empty array when levels are missing', () => {
+ expect(getTopologyLevelNames([])).toEqual([]);
+ expect(getTopologyLevelNames(null)).toEqual([]);
+ });
+ });
+});
diff --git a/kueue/src/resources/topology.ts b/kueue/src/resources/topology.ts
new file mode 100644
index 0000000000..6a3d8b46cd
--- /dev/null
+++ b/kueue/src/resources/topology.ts
@@ -0,0 +1,48 @@
+import { KubeObject, KubeObjectInterface } from '@kinvolk/headlamp-plugin/lib/k8s/cluster';
+import { kueueApiVersions } from '../utils/kueueApi';
+import { kueueRoutePaths } from '../utils/kueueRoutes';
+import {
+ getTopologyLevelNames,
+ renderTopologyLevelsCount,
+ renderTopologyLevelsSummary,
+ TopologyLevel,
+} from './topologyFormatters';
+
+export interface TopologySpec {
+ levels?: TopologyLevel[];
+}
+
+export interface KubeTopology extends KubeObjectInterface {
+ spec?: TopologySpec;
+}
+
+export class Topology extends KubeObject {
+ static kind = 'Topology';
+ static apiName = 'topologies';
+ static apiVersion = kueueApiVersions;
+ static isNamespaced = false;
+
+ static get detailsRoute() {
+ return kueueRoutePaths.topologyDetail;
+ }
+
+ get spec(): TopologySpec {
+ return this.jsonData.spec ?? {};
+ }
+
+ get levels(): TopologyLevel[] {
+ return this.spec.levels ?? [];
+ }
+
+ get levelsDisplay(): string {
+ return renderTopologyLevelsSummary(this.levels);
+ }
+
+ get levelsCount(): number {
+ return renderTopologyLevelsCount(this.levels);
+ }
+
+ get levelNames(): string[] {
+ return getTopologyLevelNames(this.levels);
+ }
+}
diff --git a/kueue/src/resources/topologyFormatters.ts b/kueue/src/resources/topologyFormatters.ts
new file mode 100644
index 0000000000..6cec6b9df8
--- /dev/null
+++ b/kueue/src/resources/topologyFormatters.ts
@@ -0,0 +1,34 @@
+export interface TopologyLevel {
+ name: string;
+ nodeLabel: string;
+}
+
+/** Render a single level's display string. */
+export function renderTopologyLevel(level?: TopologyLevel | null): string {
+ if (!level || !level.name) {
+ return '-';
+ }
+ const label = level.nodeLabel ? ` (${level.nodeLabel})` : '';
+ return `${level.name}${label}`;
+}
+
+/** Render full topology levels hierarchy string. */
+export function renderTopologyLevelsSummary(levels?: TopologyLevel[] | null): string {
+ if (!levels || levels.length === 0) {
+ return '-';
+ }
+ return levels.map(lvl => renderTopologyLevel(lvl)).join(' → ');
+}
+
+/** Render total count of defined topology levels. */
+export function renderTopologyLevelsCount(levels?: TopologyLevel[] | null): number {
+ return levels?.length ?? 0;
+}
+
+/** Extract list of level names. */
+export function getTopologyLevelNames(levels?: TopologyLevel[] | null): string[] {
+ if (!levels || levels.length === 0) {
+ return [];
+ }
+ return levels.map(l => l.name).filter(Boolean);
+}
diff --git a/kueue/src/utils/kueueRoutes.ts b/kueue/src/utils/kueueRoutes.ts
index 73fc24b62d..b1d34c1600 100644
--- a/kueue/src/utils/kueueRoutes.ts
+++ b/kueue/src/utils/kueueRoutes.ts
@@ -5,6 +5,8 @@ export const kueueRouteNames = {
localQueueDetail: 'kueue-localqueue-detail',
resourceFlavorsList: 'kueue-resourceflavors-list',
resourceFlavorDetail: 'kueue-resourceflavor-detail',
+ topologiesList: 'kueue-topologies-list',
+ topologyDetail: 'kueue-topology-detail',
workloadsList: 'kueue-workloads-list',
workloadDetail: 'kueue-workload-detail',
} as const;
@@ -16,6 +18,8 @@ export const kueueRoutePaths = {
localQueueDetail: '/kueue/localqueues/:namespace/:name',
resourceFlavorsList: '/kueue/resourceflavors',
resourceFlavorDetail: '/kueue/resourceflavors/:name',
+ topologiesList: '/kueue/topologies',
+ topologyDetail: '/kueue/topologies/:name',
workloadsList: '/kueue/workloads',
workloadDetail: '/kueue/workloads/:namespace/:name',
} as const;