Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions kueue/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -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"
}
3 changes: 3 additions & 0 deletions kueue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,8 @@
},
"overrides": {
"ws": "8.21.0"
},
"headlamp": {
"i18n": []
}
}
80 changes: 80 additions & 0 deletions kueue/src/components/topologies/Detail.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<KueueAdminResourceAccess
resourceClass={Topology}
resourceLabel={t('Topologies')}
verb="get"
accessDescription={t('Kueue Topologies define node and hardware hierarchy levels for Topology-Aware Scheduling (TAS).')}
>
<DetailsGrid
resourceType={Topology}
name={name}
withEvents
extraInfo={item =>
item
? [
{
name: t('Total Levels'),
value: item.levelsCount,
},
{
name: t('Levels Chain'),
value: item.levelsDisplay,
},
]
: []
}
extraSections={item =>
item && item.levels.length > 0
? [
<SectionBox key="topology-levels" title={t('Topology Levels Hierarchy')}>
<Box display="flex" flexDirection="column" gap={2} mt={1}>
{item.levels.map((lvl, index) => (
<Paper
key={lvl.name || index}
variant="outlined"
sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
>
<Box display="flex" alignItems="center" gap={1.5}>
<Chip
label={`Level ${index + 1}`}
color="primary"
size="small"
variant="outlined"
/>
<Typography variant="subtitle1" fontWeight="bold">
{lvl.name || t('Unnamed Level')}
</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="body2" color="text.secondary">
{t('Node Label')}:
</Typography>
<Chip
label={lvl.nodeLabel || t('None')}
size="small"
sx={{ fontFamily: 'monospace' }}
/>
</Box>
</Paper>
))}
</Box>
</SectionBox>,
]
: []
}
/>
</KueueAdminResourceAccess>
);
}
36 changes: 36 additions & 0 deletions kueue/src/components/topologies/List.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<KueueAdminResourceAccess
resourceClass={Topology}
resourceLabel={t('Topologies')}
verb="list"
accessDescription={t('Kueue Topologies define node and hardware hierarchy levels for Topology-Aware Scheduling (TAS).')}
>
<ResourceListView
title={t('Topologies')}
resourceClass={Topology}
columns={[
'name',
{
id: 'levelsDisplay',
label: t('Levels Hierarchy'),
getValue: (topology: Topology) => topology.levelsDisplay,
},
{
id: 'levelsCount',
label: t('Total Levels'),
getValue: (topology: Topology) => topology.levelsCount,
},
'age',
]}
/>
</KueueAdminResourceAccess>
);
}
25 changes: 25 additions & 0 deletions kueue/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -93,6 +102,22 @@ registerRoute({
component: () => <ResourceFlavorDetail />,
});

registerRoute({
path: kueueRoutePaths.topologiesList,
sidebar: 'kueue-topologies',
name: kueueRouteNames.topologiesList,
exact: true,
component: () => <TopologyList />,
});

registerRoute({
path: kueueRoutePaths.topologyDetail,
sidebar: 'kueue-topologies',
name: kueueRouteNames.topologyDetail,
exact: true,
component: () => <TopologyDetail />,
});

registerRoute({
path: kueueRoutePaths.workloadsList,
sidebar: 'kueue-workloads',
Expand Down
84 changes: 84 additions & 0 deletions kueue/src/resources/topology.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
});
48 changes: 48 additions & 0 deletions kueue/src/resources/topology.ts
Original file line number Diff line number Diff line change
@@ -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<KubeTopology> {
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);
}
}
34 changes: 34 additions & 0 deletions kueue/src/resources/topologyFormatters.ts
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 4 additions & 0 deletions kueue/src/utils/kueueRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;