refactor: moved connection resource usage to separate file (#16908)

* refactor: moved connection resource usage to sepparate file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>

* chore: applied suggestions

Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>

* chore: renamed file

Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>

* chore: applied suggestions

Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>

* test: extended code coverage

Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>

---------

Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Evžen Gasta 2026-04-17 12:35:00 +02:00 committed by GitHub
parent e33772fdbd
commit f7f993085f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 645 additions and 59 deletions

View file

@ -16,18 +16,19 @@
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-empty-function */
import '@testing-library/jest-dom/vitest';
import type { ProviderContainerConnectionInfo } from '@podman-desktop/core-api';
import type { IConfigurationPropertyRecordedSchema } from '@podman-desktop/core-api/configuration';
import { render, screen } from '@testing-library/svelte';
import { expect, test } from 'vitest';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import PreferencesContainerConnectionDetailsSummary from './PreferencesContainerConnectionDetailsSummary.svelte';
beforeEach(() => {
vi.resetAllMocks();
});
const podmanContainerConnection: ProviderContainerConnectionInfo = {
connectionType: 'container',
name: 'connection',
@ -83,3 +84,115 @@ test('Expect that name, socket and type are displayed for Docker', async () => {
expect(spanType).toBeInTheDocument();
expect(spanType.textContent).toBe('Docker');
});
describe('resource metrics display', () => {
const resourceProperties: IConfigurationPropertyRecordedSchema[] = [
{
parentId: 'preferences.podman',
title: 'CPUs',
id: 'podman.machine.cpus',
type: 'number',
scope: 'ContainerConnection',
format: 'cpu',
description: 'CPUs',
},
{
parentId: 'preferences.podman',
title: 'CPU Usage',
id: 'podman.machine.cpusUsage',
type: 'number',
scope: 'ContainerConnection',
format: 'cpuUsage',
description: 'CPU Usage',
hidden: true,
},
{
parentId: 'preferences.podman',
title: 'Memory',
id: 'podman.machine.memory',
type: 'number',
scope: 'ContainerConnection',
format: 'memory',
description: 'Memory',
},
{
parentId: 'preferences.podman',
title: 'Memory Usage',
id: 'podman.machine.memoryUsage',
type: 'number',
scope: 'ContainerConnection',
format: 'memoryUsage',
description: 'Memory Usage',
hidden: true,
},
];
test('renders Donut charts for resource metrics', async () => {
vi.mocked(window.getConfigurationValue)
.mockResolvedValueOnce(4)
.mockResolvedValueOnce(50)
.mockResolvedValueOnce(8_000_000_000)
.mockResolvedValueOnce(25);
render(PreferencesContainerConnectionDetailsSummary, {
containerConnectionInfo: podmanContainerConnection,
providerInternalId: '0',
properties: resourceProperties,
});
await vi.waitFor(() => {
expect(screen.getAllByTestId('arc')).toHaveLength(2);
expect(screen.getByText('4')).toBeInTheDocument();
expect(screen.getByText('8 GB')).toBeInTheDocument();
});
});
test('renders non-resource configs as plain values', async () => {
const nonResourceProperty: IConfigurationPropertyRecordedSchema = {
parentId: 'preferences.podman',
title: 'User mode networking',
id: 'podman.machine.userModeNetworking',
type: 'boolean',
scope: 'ContainerConnection',
format: 'boolean',
description: 'User mode networking',
};
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
render(PreferencesContainerConnectionDetailsSummary, {
containerConnectionInfo: podmanContainerConnection,
providerInternalId: '0',
properties: [...resourceProperties, nonResourceProperty],
});
await vi.waitFor(() => {
expect(screen.getByText('User mode networking')).toBeInTheDocument();
});
});
test('does not render resource-format configs as plain rows', async () => {
vi.mocked(window.getConfigurationValue).mockResolvedValue(4);
render(PreferencesContainerConnectionDetailsSummary, {
containerConnectionInfo: podmanContainerConnection,
providerInternalId: '0',
properties: resourceProperties,
});
await vi.waitFor(() => {
expect(screen.getAllByTestId('arc')).toHaveLength(2);
});
expect(screen.queryByText('CPU Usage')).not.toBeInTheDocument();
expect(screen.queryByText('Memory Usage')).not.toBeInTheDocument();
});
test('renders no metrics when properties is empty', async () => {
render(PreferencesContainerConnectionDetailsSummary, {
containerConnectionInfo: podmanContainerConnection,
providerInternalId: '0',
properties: [],
});
expect(screen.queryAllByTestId('arc')).toHaveLength(0);
});
});

View file

@ -2,11 +2,10 @@
import type { ContainerProviderConnection } from '@podman-desktop/api';
import type { ProviderContainerConnectionInfo } from '@podman-desktop/core-api';
import type { IConfigurationPropertyRecordedSchema } from '@podman-desktop/core-api/configuration';
import { filesize } from 'filesize';
import Donut from '/@/lib/donut/Donut.svelte';
import { PeerProperties } from './PeerProperties';
import { extractConnectionResourceMetrics, RESOURCE_FORMATS, toDisplayMetrics } from './connection-resource-metrics';
import type { IProviderConnectionConfigurationPropertyRecorded } from './Util';
interface Props {
@ -18,6 +17,11 @@ interface Props {
const { properties = [], providerInternalId, containerConnectionInfo }: Props = $props();
let providerContainerConfiguration: IProviderConnectionConfigurationPropertyRecorded[] = $state([]);
let resourceMetrics = $derived(extractConnectionResourceMetrics(providerContainerConfiguration));
let displayMetrics = $derived(resourceMetrics ? toDisplayMetrics(resourceMetrics) : []);
let nonResourceConfigs = $derived(
providerContainerConfiguration.filter(conf => !RESOURCE_FORMATS.has(conf.format ?? '') && !conf.hidden),
);
$effect(() => {
Promise.all(
@ -42,31 +46,21 @@ $effect(() => {
<div class="h-full text-[var(--pd-details-body-text)]">
{#if containerConnectionInfo}
{@const peerProperties = new PeerProperties()}
<div class="flex pl-8 py-4 flex-col w-full text-sm">
<div class="flex flex-row mt-5">
<span class="font-semibold min-w-[150px]">Name</span>
<span aria-label={containerConnectionInfo.name}>{containerConnectionInfo.name}</span>
</div>
{#each providerContainerConfiguration as connectionSetting (connectionSetting.id)}
{#each displayMetrics as metric (metric.title)}
<div class="flex flex-row mt-5">
<span class="font-semibold min-w-[150px]">{metric.title}</span>
<Donut title={metric.title} value={metric.value} percent={metric.percent} />
</div>
{/each}
{#each nonResourceConfigs as connectionSetting (connectionSetting.id)}
<div class="flex flex-row mt-5">
<span class="font-semibold min-w-[150px]">{connectionSetting.description}</span>
{#if connectionSetting.format === 'cpu' || connectionSetting.format === 'cpuUsage'}
{#if !peerProperties.isPeerProperty(connectionSetting.id)}
{@const peerValue = peerProperties.getPeerProperty(connectionSetting.id, providerContainerConfiguration)}
<Donut title={connectionSetting.description} value={connectionSetting.value} percent={peerValue} />
{/if}
{:else if connectionSetting.format === 'memory' || connectionSetting.format === 'memoryUsage' || connectionSetting.format === 'diskSize' || connectionSetting.format === 'diskSizeUsage'}
{#if !peerProperties.isPeerProperty(connectionSetting.id)}
{@const peerValue = peerProperties.getPeerProperty(connectionSetting.id, providerContainerConfiguration)}
<Donut
title={connectionSetting.description}
value={filesize(connectionSetting.value)}
percent={peerValue} />
{/if}
{:else}
<span>{connectionSetting.value}</span>
{/if}
<span>{connectionSetting.value}</span>
</div>
{/each}
<div class="flex flex-row mt-5">

View file

@ -838,6 +838,130 @@ describe('container provider connections', () => {
});
});
describe('container connection resource metrics', () => {
const resourceConfigProperties = [
{
parentId: 'preferences.podman',
title: 'CPUs',
id: 'podman.machine.cpus',
type: 'number' as const,
scope: 'ContainerConnection' as const,
format: 'cpu',
description: 'CPUs',
},
{
parentId: 'preferences.podman',
title: 'CPU Usage',
id: 'podman.machine.cpusUsage',
type: 'number' as const,
scope: 'ContainerConnection' as const,
format: 'cpuUsage',
description: 'CPU Usage',
hidden: true,
},
{
parentId: 'preferences.podman',
title: 'Memory',
id: 'podman.machine.memory',
type: 'number' as const,
scope: 'ContainerConnection' as const,
format: 'memory',
description: 'Memory',
},
{
parentId: 'preferences.podman',
title: 'Memory Usage',
id: 'podman.machine.memoryUsage',
type: 'number' as const,
scope: 'ContainerConnection' as const,
format: 'memoryUsage',
description: 'Memory Usage',
hidden: true,
},
{
parentId: 'preferences.podman',
title: 'Disk Size',
id: 'podman.machine.diskSize',
type: 'number' as const,
scope: 'ContainerConnection' as const,
format: 'diskSize',
description: 'Disk size',
},
{
parentId: 'preferences.podman',
title: 'Disk Size Usage',
id: 'podman.machine.diskSizeUsage',
type: 'number' as const,
scope: 'ContainerConnection' as const,
format: 'diskSizeUsage',
description: 'Disk Size Usage',
hidden: true,
},
];
test('renders Donut charts when resource metrics are available', async () => {
const singleProvider: ProviderInfo = structuredClone(providerInfo);
singleProvider.containerConnections = [providerInfo.containerConnections[0]];
providerInfos.set([singleProvider]);
configurationProperties.set(resourceConfigProperties);
vi.mocked(window.getConfigurationValue).mockResolvedValue(4);
render(PreferencesResourcesRendering, {});
await vi.waitFor(() => {
const configGroup = screen.getByRole('group', { name: 'Provider Configuration' });
expect(configGroup).toBeInTheDocument();
expect(within(configGroup).getByText('CPUs')).toBeInTheDocument();
expect(within(configGroup).getByText('Memory')).toBeInTheDocument();
expect(within(configGroup).getByText('Disk size')).toBeInTheDocument();
});
});
test('renders non-resource configs as plain values', async () => {
const nonResourceConfig = {
parentId: 'preferences.podman',
title: 'User mode networking',
id: 'podman.machine.userModeNetworking',
type: 'boolean' as const,
scope: 'ContainerConnection' as const,
format: 'boolean',
description: 'User mode networking',
};
const singleProvider: ProviderInfo = structuredClone(providerInfo);
singleProvider.containerConnections = [providerInfo.containerConnections[0]];
providerInfos.set([singleProvider]);
configurationProperties.set([...resourceConfigProperties, nonResourceConfig]);
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
render(PreferencesResourcesRendering, {});
await vi.waitFor(() => {
const configGroup = screen.getByRole('group', { name: 'Provider Configuration' });
expect(configGroup).toBeInTheDocument();
expect(within(configGroup).getByText(/User mode networking/)).toBeInTheDocument();
});
});
test('does not render resource-format configs as plain text rows', async () => {
const singleProvider: ProviderInfo = structuredClone(providerInfo);
singleProvider.containerConnections = [providerInfo.containerConnections[0]];
providerInfos.set([singleProvider]);
configurationProperties.set(resourceConfigProperties);
vi.mocked(window.getConfigurationValue).mockResolvedValue(4);
render(PreferencesResourcesRendering, {});
await vi.waitFor(() => {
const configGroup = screen.getByRole('group', { name: 'Provider Configuration' });
expect(configGroup).toBeInTheDocument();
});
const configGroup = screen.getByRole('group', { name: 'Provider Configuration' });
expect(within(configGroup).queryByText(/CPU Usage: /)).not.toBeInTheDocument();
expect(within(configGroup).queryByText(/Memory Usage: /)).not.toBeInTheDocument();
expect(within(configGroup).queryByText(/Disk Size Usage: /)).not.toBeInTheDocument();
});
});
test('Expect to see the no resource message when there is no providers', async () => {
providerInfos.set([]);
render(PreferencesResourcesRendering, {});

View file

@ -7,7 +7,6 @@ import type { IConfigurationPropertyRecordedSchema } from '@podman-desktop/core-
import { DropdownMenu, EmptyScreen, Tooltip } from '@podman-desktop/ui-svelte';
import { Icon } from '@podman-desktop/ui-svelte/icons';
import { Buffer } from 'buffer';
import { filesize } from 'filesize';
import { onDestroy, onMount } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';
import type { Unsubscriber } from 'svelte/store';
@ -29,7 +28,12 @@ import { context } from '/@/stores/context';
import { onboardingList } from '/@/stores/onboarding';
import { providerInfos } from '/@/stores/providers';
import { PeerProperties } from './PeerProperties';
import {
type ConnectionResourceMetricDisplay,
extractConnectionResourceMetrics,
RESOURCE_FORMATS,
toDisplayMetrics,
} from './connection-resource-metrics';
import { eventCollect } from './preferences-connection-rendering-task';
import PreferencesConnectionActions from './PreferencesConnectionActions.svelte';
import PreferencesConnectionsEmptyRendering from './PreferencesConnectionsEmptyRendering.svelte';
@ -364,6 +368,27 @@ function getRootfulDisplayInfo(
return rootfulSetting;
}
function getConnectionResourceData(
provider: ProviderInfo,
container: ProviderConnectionInfo,
):
| {
displayMetrics: ConnectionResourceMetricDisplay[];
nonResourceConfigs: IProviderConnectionConfigurationPropertyRecorded[];
}
| undefined {
if (!providerContainerConfiguration.has(provider.internalId)) {
return undefined;
}
const connectionConfigs = (providerContainerConfiguration.get(provider.internalId) ?? []).filter(
conf => conf.connection === container.name,
);
const resourceMetrics = extractConnectionResourceMetrics(connectionConfigs);
const displayMetrics = resourceMetrics ? toDisplayMetrics(resourceMetrics) : [];
const nonResourceConfigs = connectionConfigs.filter(conf => !RESOURCE_FORMATS.has(conf.format ?? '') && !conf.hidden);
return { displayMetrics, nonResourceConfigs };
}
let { properties = [], focus }: Props = $props();
let providerElementMap = $state<Record<string, HTMLElement>>({});
@ -488,7 +513,6 @@ $effect(() => {
message={provider.emptyConnectionMarkdownDescription}
hidden={provider.containerConnections.length > 0 || provider.kubernetesConnections.length > 0 || provider.vmConnections.length > 0} />
{#each provider.containerConnections as container, index (index)}
{@const peerProperties = new PeerProperties()}
{@const rootfulInfo = getRootfulDisplayInfo(provider, container)}
<div class="px-5 py-2 w-[240px] border-r border-[var(--pd-content-divider)]" role="region" aria-label={container.name}>
<div class="float-right">
@ -531,41 +555,18 @@ $effect(() => {
class={container.status !== 'started' ? 'text-[var(--pd-content-sub-header)]' : ''}
path={container.endpoint.socketPath} />
{#if providerContainerConfiguration.has(provider.internalId)}
{@const providerConfiguration = providerContainerConfiguration.get(provider.internalId) ?? []}
{@const { displayMetrics, nonResourceConfigs } = getConnectionResourceData(provider, container)!}
<div
class="flex mt-3 {container.status !== 'started' ? 'text-[var(--pd-content-sub-header)]' : ''}"
role="group"
aria-label="Provider Configuration">
{#each providerConfiguration.filter(conf => conf.connection === container.name) as connectionSetting (connectionSetting.id)}
{#if connectionSetting.format === 'cpu' || connectionSetting.format === 'cpuUsage'}
{#if !peerProperties.isPeerProperty(connectionSetting.id)}
{@const peerValue = peerProperties.getPeerProperty(
connectionSetting.id,
providerConfiguration.filter(conf => conf.connection === container.name),
)}
<div class="mr-4">
<Donut
title={connectionSetting.description}
value={connectionSetting.value}
percent={peerValue} />
</div>
{/if}
{:else if connectionSetting.format === 'memory' || connectionSetting.format === 'memoryUsage' || connectionSetting.format === 'diskSize' || connectionSetting.format === 'diskSizeUsage'}
{#if !peerProperties.isPeerProperty(connectionSetting.id)}
{@const peerValue = peerProperties.getPeerProperty(
connectionSetting.id,
providerConfiguration.filter(conf => conf.connection === container.name),
)}
<div class="mr-4">
<Donut
title={connectionSetting.description}
value={filesize(connectionSetting.value)}
percent={peerValue} />
</div>
{/if}
{:else if !connectionSetting.hidden}
{connectionSetting.description}: {connectionSetting.value}
{/if}
{#each displayMetrics as metric (metric.title)}
<div class="mr-4">
<Donut title={metric.title} value={metric.value} percent={metric.percent} />
</div>
{/each}
{#each nonResourceConfigs as connectionSetting (connectionSetting.id)}
{connectionSetting.description}: {connectionSetting.value}
{/each}
</div>
{/if}

View file

@ -0,0 +1,252 @@
/**********************************************************************
* Copyright (C) 2026 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import { assert, describe, expect, test } from 'vitest';
import type { ConnectionResourceMetrics } from './connection-resource-metrics';
import { extractConnectionResourceMetrics, RESOURCE_FORMATS, toDisplayMetrics } from './connection-resource-metrics';
import type { IProviderConnectionConfigurationPropertyRecorded } from './Util';
function makeConfig(
overrides: Partial<IProviderConnectionConfigurationPropertyRecorded> & { id: string },
): IProviderConnectionConfigurationPropertyRecorded {
return {
title: '',
parentId: '',
type: 'number',
connection: 'machine1',
providerId: 'provider1',
...overrides,
};
}
describe('extractConnectionResourceMetrics', () => {
test('returns undefined for empty configs', () => {
expect(extractConnectionResourceMetrics([])).toBeUndefined();
});
test('returns undefined when all values are undefined', () => {
const configs = [makeConfig({ id: 'podman.machine.cpus', format: 'cpu', value: undefined })];
expect(extractConnectionResourceMetrics(configs)).toBeUndefined();
});
test('returns undefined when no resource formats are present', () => {
const configs = [makeConfig({ id: 'podman.machine.rootful', format: 'boolean', value: true })];
expect(extractConnectionResourceMetrics(configs)).toBeUndefined();
});
test('extracts CPU metrics with usage peer', () => {
const configs = [
makeConfig({ id: 'podman.machine.cpus', format: 'cpu', value: 4, description: 'CPUs' }),
makeConfig({ id: 'podman.machine.cpusUsage', format: 'cpuUsage', value: 50 }),
];
const result = extractConnectionResourceMetrics(configs);
assert(result);
expect(result.cpu).toEqual({
total: 4,
used: 2,
usagePercent: 50,
description: 'CPUs',
});
});
test('extracts memory metrics with usage peer', () => {
const configs = [
makeConfig({
id: 'podman.machine.memory',
format: 'memory',
value: 8_000_000_000,
description: 'Memory',
}),
makeConfig({ id: 'podman.machine.memoryUsage', format: 'memoryUsage', value: 25 }),
];
const result = extractConnectionResourceMetrics(configs);
assert(result);
expect(result.memory).toEqual({
total: 8_000_000_000,
used: 2_000_000_000,
usagePercent: 25,
description: 'Memory',
});
});
test('extracts disk metrics with usage peer', () => {
const configs = [
makeConfig({
id: 'podman.machine.diskSize',
format: 'diskSize',
value: 100_000_000_000,
description: 'Disk size',
}),
makeConfig({ id: 'podman.machine.diskSizeUsage', format: 'diskSizeUsage', value: 40 }),
];
const result = extractConnectionResourceMetrics(configs);
assert(result);
expect(result.disk).toEqual({
total: 100_000_000_000,
used: 40_000_000_000,
usagePercent: 40,
description: 'Disk size',
});
});
test('extracts all three metrics together', () => {
const configs = [
makeConfig({ id: 'podman.machine.cpus', format: 'cpu', value: 8 }),
makeConfig({ id: 'podman.machine.cpusUsage', format: 'cpuUsage', value: 75 }),
makeConfig({ id: 'podman.machine.memory', format: 'memory', value: 16_000_000_000 }),
makeConfig({ id: 'podman.machine.memoryUsage', format: 'memoryUsage', value: 50 }),
makeConfig({ id: 'podman.machine.diskSize', format: 'diskSize', value: 200_000_000_000 }),
makeConfig({ id: 'podman.machine.diskSizeUsage', format: 'diskSizeUsage', value: 30 }),
];
const result = extractConnectionResourceMetrics(configs);
assert(result);
expect(result.cpu?.total).toBe(8);
expect(result.cpu?.usagePercent).toBe(75);
expect(result.memory?.total).toBe(16_000_000_000);
expect(result.memory?.usagePercent).toBe(50);
expect(result.disk?.total).toBe(200_000_000_000);
expect(result.disk?.usagePercent).toBe(30);
});
test('handles missing usage peer gracefully', () => {
const configs = [makeConfig({ id: 'podman.machine.cpus', format: 'cpu', value: 4, description: 'CPUs' })];
const result = extractConnectionResourceMetrics(configs);
assert(result);
expect(result.cpu).toEqual({
total: 4,
used: 0,
usagePercent: 0,
description: 'CPUs',
});
});
test('returns partial metrics when only some formats exist', () => {
const configs = [
makeConfig({ id: 'podman.machine.cpus', format: 'cpu', value: 2 }),
makeConfig({ id: 'podman.machine.cpusUsage', format: 'cpuUsage', value: 10 }),
];
const result = extractConnectionResourceMetrics(configs);
assert(result);
expect(result.cpu).toBeDefined();
expect(result.memory).toBeUndefined();
expect(result.disk).toBeUndefined();
});
});
describe('toDisplayMetrics', () => {
test('returns empty array for empty metrics', () => {
const metrics: ConnectionResourceMetrics = {};
expect(toDisplayMetrics(metrics)).toEqual([]);
});
test('formats CPU metric with raw number value', () => {
const metrics: ConnectionResourceMetrics = {
cpu: { total: 4, used: 2, usagePercent: 50, description: 'CPUs' },
};
const result = toDisplayMetrics(metrics);
expect(result).toEqual([{ title: 'CPUs', value: 4, percent: 50 }]);
});
test('formats memory metric with filesize', () => {
const metrics: ConnectionResourceMetrics = {
memory: { total: 8_000_000_000, used: 2_000_000_000, usagePercent: 25, description: 'Memory' },
};
const result = toDisplayMetrics(metrics);
expect(result).toHaveLength(1);
expect(result[0].title).toBe('Memory');
expect(result[0].value).toBe('8 GB');
expect(result[0].percent).toBe(25);
});
test('formats disk metric with filesize', () => {
const metrics: ConnectionResourceMetrics = {
disk: { total: 100_000_000_000, used: 40_000_000_000, usagePercent: 40, description: 'Disk size' },
};
const result = toDisplayMetrics(metrics);
expect(result).toHaveLength(1);
expect(result[0].title).toBe('Disk size');
expect(result[0].value).toBe('100 GB');
expect(result[0].percent).toBe(40);
});
test('returns all three metrics in cpu/memory/disk order', () => {
const metrics: ConnectionResourceMetrics = {
cpu: { total: 8, used: 6, usagePercent: 75, description: 'CPUs' },
memory: { total: 16_000_000_000, used: 8_000_000_000, usagePercent: 50, description: 'Memory' },
disk: { total: 200_000_000_000, used: 60_000_000_000, usagePercent: 30, description: 'Disk size' },
};
const result = toDisplayMetrics(metrics);
expect(result).toHaveLength(3);
expect(result[0].title).toBe('CPUs');
expect(result[0].value).toBe(8);
expect(result[1].title).toBe('Memory');
expect(result[1].value).toBe('16 GB');
expect(result[2].title).toBe('Disk size');
expect(result[2].value).toBe('200 GB');
});
test('returns partial results for partial metrics', () => {
const metrics: ConnectionResourceMetrics = {
cpu: { total: 2, used: 0.2, usagePercent: 10, description: 'CPUs' },
disk: { total: 50_000_000_000, used: 5_000_000_000, usagePercent: 10, description: 'Disk size' },
};
const result = toDisplayMetrics(metrics);
expect(result).toHaveLength(2);
expect(result[0].title).toBe('CPUs');
expect(result[1].title).toBe('Disk size');
});
});
describe('RESOURCE_FORMATS', () => {
test('contains all resource format types', () => {
expect(RESOURCE_FORMATS).toContain('cpu');
expect(RESOURCE_FORMATS).toContain('cpuUsage');
expect(RESOURCE_FORMATS).toContain('memory');
expect(RESOURCE_FORMATS).toContain('memoryUsage');
expect(RESOURCE_FORMATS).toContain('diskSize');
expect(RESOURCE_FORMATS).toContain('diskSizeUsage');
});
test('does not contain non-resource formats', () => {
expect(RESOURCE_FORMATS).not.toContain('boolean');
expect(RESOURCE_FORMATS).not.toContain('string');
});
});

View file

@ -0,0 +1,102 @@
/**********************************************************************
* Copyright (C) 2026 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import { filesize } from 'filesize';
import { PeerProperties } from './PeerProperties';
import type { IProviderConnectionConfigurationPropertyRecorded } from './Util';
export interface ConnectionResourceMetric {
total: number;
used: number;
usagePercent: number;
description?: string;
}
export interface ConnectionResourceMetrics {
cpu?: ConnectionResourceMetric;
memory?: ConnectionResourceMetric;
disk?: ConnectionResourceMetric;
}
export const RESOURCE_FORMATS = new Set(['cpu', 'cpuUsage', 'memory', 'memoryUsage', 'diskSize', 'diskSizeUsage']);
function extractMetric(
format: string,
connectionConfigs: IProviderConnectionConfigurationPropertyRecorded[],
peerProperties: PeerProperties,
): ConnectionResourceMetric | undefined {
const config = connectionConfigs.find(c => c.format === format);
if (!config) return undefined;
const usagePercent = peerProperties.getPeerProperty(config.id, connectionConfigs) ?? 0;
const total = config.value ?? 0;
const used = total > 0 && usagePercent > 0 ? (usagePercent / 100) * total : 0;
return {
total,
used,
usagePercent: typeof usagePercent === 'number' ? usagePercent : 0,
description: config.description,
};
}
export function extractConnectionResourceMetrics(
configs: IProviderConnectionConfigurationPropertyRecorded[],
): ConnectionResourceMetrics | undefined {
const connectionConfigs = configs.filter(config => config.value !== undefined);
if (connectionConfigs.length === 0) return undefined;
const peerProperties = new PeerProperties();
const cpu = extractMetric('cpu', connectionConfigs, peerProperties);
const memory = extractMetric('memory', connectionConfigs, peerProperties);
const disk = extractMetric('diskSize', connectionConfigs, peerProperties);
if (!cpu && !memory && !disk) return undefined;
return { cpu, memory, disk };
}
export interface ConnectionResourceMetricDisplay {
title?: string;
value: string | number;
percent: number;
}
export function toDisplayMetrics(metrics: ConnectionResourceMetrics): ConnectionResourceMetricDisplay[] {
const entries: ConnectionResourceMetricDisplay[] = [];
if (metrics.cpu) {
entries.push({ title: metrics.cpu.description, value: metrics.cpu.total, percent: metrics.cpu.usagePercent });
}
if (metrics.memory) {
entries.push({
title: metrics.memory.description,
value: filesize(metrics.memory.total),
percent: metrics.memory.usagePercent,
});
}
if (metrics.disk) {
entries.push({
title: metrics.disk.description,
value: filesize(metrics.disk.total),
percent: metrics.disk.usagePercent,
});
}
return entries;
}