mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-04-21 09:37:22 +00:00
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:
parent
e33772fdbd
commit
f7f993085f
6 changed files with 645 additions and 59 deletions
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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, {});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in a new issue