mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
chore(react): Convert cluster details to react CE (#466)
This commit is contained in:
parent
dd98097897
commit
7759d762ab
24 changed files with 829 additions and 345 deletions
|
@ -22,6 +22,7 @@ import { VolumesView } from '@/react/kubernetes/volumes/ListView/VolumesView';
|
|||
import { NamespaceView } from '@/react/kubernetes/namespaces/ItemView/NamespaceView';
|
||||
import { AccessView } from '@/react/kubernetes/namespaces/AccessView/AccessView';
|
||||
import { JobsView } from '@/react/kubernetes/more-resources/JobsView/JobsView';
|
||||
import { ClusterView } from '@/react/kubernetes/cluster/ClusterView';
|
||||
|
||||
export const viewsModule = angular
|
||||
.module('portainer.kubernetes.react.views', [])
|
||||
|
@ -78,6 +79,10 @@ export const viewsModule = angular
|
|||
[]
|
||||
)
|
||||
)
|
||||
.component(
|
||||
'kubernetesClusterView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ClusterView))), [])
|
||||
)
|
||||
.component(
|
||||
'kubernetesConfigureView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ConfigureView))), [])
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
<page-header ng-if="ctrl.state.viewReady" title="'Cluster'" breadcrumbs="['Cluster information']" reload="true"></page-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row" ng-if="ctrl.isAdmin">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<!-- resource-reservation -->
|
||||
<form class="form-horizontal" ng-if="ctrl.resourceReservation">
|
||||
<kubernetes-resource-reservation
|
||||
description="Resource reservation represents the total amount of resource assigned to all the applications inside the cluster."
|
||||
cpu-reservation="ctrl.resourceReservation.CPU"
|
||||
cpu-limit="ctrl.CPULimit"
|
||||
memory-reservation="ctrl.resourceReservation.Memory"
|
||||
memory-limit="ctrl.MemoryLimit"
|
||||
display-usage="ctrl.hasResourceUsageAccess()"
|
||||
cpu-usage="ctrl.resourceUsage.CPU"
|
||||
memory-usage="ctrl.resourceUsage.Memory"
|
||||
>
|
||||
</kubernetes-resource-reservation>
|
||||
</form>
|
||||
<!-- !resource-reservation -->
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<kube-nodes-datatable></kube-nodes-datatable>
|
||||
</div>
|
||||
</div>
|
|
@ -1,8 +0,0 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesClusterView', {
|
||||
templateUrl: './cluster.html',
|
||||
controller: 'KubernetesClusterController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
|
@ -1,139 +0,0 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
|
||||
import { getMetricsForAllNodes, getTotalResourcesForAllApplications } from '@/react/kubernetes/metrics/metrics.ts';
|
||||
|
||||
class KubernetesClusterController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, LocalStorage, Authentication, KubernetesNodeService, KubernetesApplicationService, KubernetesEndpointService, EndpointService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Authentication = Authentication;
|
||||
this.Notifications = Notifications;
|
||||
this.LocalStorage = LocalStorage;
|
||||
this.KubernetesNodeService = KubernetesNodeService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.KubernetesEndpointService = KubernetesEndpointService;
|
||||
this.EndpointService = EndpointService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getNodes = this.getNodes.bind(this);
|
||||
this.getNodesAsync = this.getNodesAsync.bind(this);
|
||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
||||
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
|
||||
this.hasResourceUsageAccess = this.hasResourceUsageAccess.bind(this);
|
||||
}
|
||||
|
||||
async getEndpointsAsync() {
|
||||
try {
|
||||
const endpoints = await this.KubernetesEndpointService.get();
|
||||
const systemEndpoints = _.filter(endpoints, { Namespace: 'kube-system' });
|
||||
this.systemEndpoints = _.filter(systemEndpoints, (ep) => ep.HolderIdentity);
|
||||
|
||||
const kubernetesEndpoint = _.find(endpoints, { Name: 'kubernetes' });
|
||||
if (kubernetesEndpoint && kubernetesEndpoint.Subsets) {
|
||||
const ips = _.flatten(_.map(kubernetesEndpoint.Subsets, 'Ips'));
|
||||
_.forEach(this.nodes, (node) => {
|
||||
node.Api = _.includes(ips, node.IPAddress);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve environments');
|
||||
}
|
||||
}
|
||||
|
||||
getEndpoints() {
|
||||
return this.$async(this.getEndpointsAsync);
|
||||
}
|
||||
|
||||
async getNodesAsync() {
|
||||
try {
|
||||
const nodes = await this.KubernetesNodeService.get();
|
||||
_.forEach(nodes, (node) => (node.Memory = filesizeParser(node.Memory)));
|
||||
this.nodes = nodes;
|
||||
this.CPULimit = _.reduce(this.nodes, (acc, node) => node.CPU + acc, 0);
|
||||
this.CPULimit = Math.round(this.CPULimit * 10000) / 10000;
|
||||
this.MemoryLimit = _.reduce(this.nodes, (acc, node) => KubernetesResourceReservationHelper.megaBytesValue(node.Memory) + acc, 0);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve nodes');
|
||||
}
|
||||
}
|
||||
|
||||
getNodes() {
|
||||
return this.$async(this.getNodesAsync);
|
||||
}
|
||||
|
||||
async getApplicationsAsync() {
|
||||
try {
|
||||
this.state.applicationsLoading = true;
|
||||
|
||||
const applicationsResources = await getTotalResourcesForAllApplications(this.endpoint.Id);
|
||||
this.resourceReservation = new KubernetesResourceReservation();
|
||||
this.resourceReservation.CPU = Math.round(applicationsResources.CpuRequest / 1000);
|
||||
this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(applicationsResources.MemoryRequest);
|
||||
|
||||
if (this.hasResourceUsageAccess()) {
|
||||
await this.getResourceUsage(this.endpoint.Id);
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
||||
} finally {
|
||||
this.state.applicationsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getApplications() {
|
||||
return this.$async(this.getApplicationsAsync);
|
||||
}
|
||||
|
||||
async getResourceUsage(endpointId) {
|
||||
try {
|
||||
const nodeMetrics = await getMetricsForAllNodes(endpointId);
|
||||
const resourceUsageList = nodeMetrics.items.map((i) => i.usage);
|
||||
const clusterResourceUsage = resourceUsageList.reduce((total, u) => {
|
||||
total.CPU += KubernetesResourceReservationHelper.parseCPU(u.cpu);
|
||||
total.Memory += KubernetesResourceReservationHelper.megaBytesValue(u.memory);
|
||||
return total;
|
||||
}, new KubernetesResourceReservation());
|
||||
this.resourceUsage = clusterResourceUsage;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve cluster resource usage');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if resource usage stats can be displayed
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasResourceUsageAccess() {
|
||||
return this.isAdmin && this.state.useServerMetrics;
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.endpoint = await this.EndpointService.endpoint(this.endpoint.Id);
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
const useServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||
|
||||
this.state = {
|
||||
applicationsLoading: true,
|
||||
viewReady: false,
|
||||
useServerMetrics,
|
||||
};
|
||||
|
||||
await this.getNodes();
|
||||
if (this.isAdmin) {
|
||||
await Promise.allSettled([this.getEndpoints(), this.getApplicationsAsync()]);
|
||||
}
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesClusterController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesClusterController', KubernetesClusterController);
|
|
@ -1,4 +1,5 @@
|
|||
import _ from 'lodash';
|
||||
import { QueryObserverResult } from '@tanstack/react-query';
|
||||
|
||||
import { Team } from '@/react/portainer/users/teams/types';
|
||||
import { Role, User, UserId } from '@/portainer/users/types';
|
||||
|
@ -134,3 +135,38 @@ export function createMockEnvironment(): Environment {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockQueryResult<TData, TError = unknown>(
|
||||
data: TData,
|
||||
overrides?: Partial<QueryObserverResult<TData, TError>>
|
||||
) {
|
||||
const defaultResult = {
|
||||
data,
|
||||
dataUpdatedAt: 0,
|
||||
error: null,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
errorUpdateCount: 0,
|
||||
failureReason: null,
|
||||
isError: false,
|
||||
isFetched: true,
|
||||
isFetchedAfterMount: true,
|
||||
isFetching: false,
|
||||
isInitialLoading: false,
|
||||
isLoading: false,
|
||||
isLoadingError: false,
|
||||
isPaused: false,
|
||||
isPlaceholderData: false,
|
||||
isPreviousData: false,
|
||||
isRefetchError: false,
|
||||
isRefetching: false,
|
||||
isStale: false,
|
||||
isSuccess: true,
|
||||
refetch: async () => defaultResult,
|
||||
remove: () => {},
|
||||
status: 'success',
|
||||
fetchStatus: 'idle',
|
||||
};
|
||||
|
||||
return { ...defaultResult, ...overrides };
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ComponentProps, PropsWithChildren, ReactNode } from 'react';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
|
@ -10,10 +10,11 @@ export type Size = 'xsmall' | 'small' | 'medium' | 'large' | 'vertical';
|
|||
|
||||
export interface Props {
|
||||
inputId?: string;
|
||||
dataCy?: string;
|
||||
label: ReactNode;
|
||||
size?: Size;
|
||||
tooltip?: ComponentProps<typeof Tooltip>['message'];
|
||||
setTooltipHtmlMessage?: ComponentProps<typeof Tooltip>['setHtmlMessage'];
|
||||
tooltip?: ReactNode;
|
||||
setTooltipHtmlMessage?: boolean;
|
||||
children: ReactNode;
|
||||
errors?: ReactNode;
|
||||
required?: boolean;
|
||||
|
@ -24,6 +25,7 @@ export interface Props {
|
|||
|
||||
export function FormControl({
|
||||
inputId,
|
||||
dataCy,
|
||||
label,
|
||||
size = 'small',
|
||||
tooltip = '',
|
||||
|
@ -42,6 +44,7 @@ export function FormControl({
|
|||
'form-group',
|
||||
'after:clear-both after:table after:content-[""]' // to fix issues with float
|
||||
)}
|
||||
data-cy={dataCy}
|
||||
>
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
|
@ -56,10 +59,15 @@ export function FormControl({
|
|||
)}
|
||||
</label>
|
||||
|
||||
<div className={sizeClassChildren(size)}>
|
||||
{isLoading && <InlineLoader>{loadingText}</InlineLoader>}
|
||||
<div className={clsx('flex flex-col', sizeClassChildren(size))}>
|
||||
{isLoading && (
|
||||
// 34px height to reduce layout shift when loading is complete
|
||||
<div className="h-[34px] flex items-center">
|
||||
<InlineLoader>{loadingText}</InlineLoader>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && children}
|
||||
{errors && <FormError>{errors}</FormError>}
|
||||
{!!errors && !isLoading && <FormError>{errors}</FormError>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
import { render, screen, within } from '@testing-library/react';
|
||||
import { HttpResponse } from 'msw';
|
||||
|
||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
import { server, http } from '@/setup-tests/server';
|
||||
import {
|
||||
createMockEnvironment,
|
||||
createMockQueryResult,
|
||||
} from '@/react-tools/test-mocks';
|
||||
|
||||
import { ClusterResourceReservation } from './ClusterResourceReservation';
|
||||
|
||||
const mockUseAuthorizations = vi.fn();
|
||||
const mockUseEnvironmentId = vi.fn(() => 3);
|
||||
const mockUseCurrentEnvironment = vi.fn();
|
||||
|
||||
// Set up mock implementations for hooks
|
||||
vi.mock('@/react/hooks/useUser', () => ({
|
||||
useAuthorizations: () => mockUseAuthorizations(),
|
||||
}));
|
||||
|
||||
vi.mock('@/react/hooks/useEnvironmentId', () => ({
|
||||
useEnvironmentId: () => mockUseEnvironmentId(),
|
||||
}));
|
||||
|
||||
vi.mock('@/react/hooks/useCurrentEnvironment', () => ({
|
||||
useCurrentEnvironment: () => mockUseCurrentEnvironment(),
|
||||
}));
|
||||
|
||||
function renderComponent() {
|
||||
const Wrapped = withTestQueryProvider(ClusterResourceReservation);
|
||||
return render(<Wrapped />);
|
||||
}
|
||||
|
||||
describe('ClusterResourceReservation', () => {
|
||||
beforeEach(() => {
|
||||
// Set the return values for the hooks
|
||||
mockUseAuthorizations.mockReturnValue({
|
||||
authorized: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
mockUseEnvironmentId.mockReturnValue(3);
|
||||
|
||||
const mockEnvironment = createMockEnvironment();
|
||||
mockEnvironment.Kubernetes.Configuration.UseServerMetrics = true;
|
||||
mockUseCurrentEnvironment.mockReturnValue(
|
||||
createMockQueryResult(mockEnvironment)
|
||||
);
|
||||
|
||||
// Setup default mock responses
|
||||
server.use(
|
||||
http.get('/api/endpoints/3/kubernetes/api/v1/nodes', () =>
|
||||
HttpResponse.json({
|
||||
items: [
|
||||
{
|
||||
status: {
|
||||
allocatable: {
|
||||
cpu: '4',
|
||||
memory: '8Gi',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
http.get('/api/kubernetes/3/metrics/nodes', () =>
|
||||
HttpResponse.json({
|
||||
items: [
|
||||
{
|
||||
usage: {
|
||||
cpu: '2',
|
||||
memory: '4Gi',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
http.get('/api/kubernetes/3/metrics/applications_resources', () =>
|
||||
HttpResponse.json({
|
||||
CpuRequest: 1000,
|
||||
MemoryRequest: '2Gi',
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should display resource limits, reservations and usage when all APIs respond successfully', async () => {
|
||||
renderComponent();
|
||||
|
||||
expect(
|
||||
await within(await screen.findByTestId('memory-reservation')).findByText(
|
||||
'2147 / 8589 MB - 25%'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await within(await screen.findByTestId('memory-usage')).findByText(
|
||||
'4294 / 8589 MB - 50%'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await within(await screen.findByTestId('cpu-reservation')).findByText(
|
||||
'1 / 4 - 25%'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await within(await screen.findByTestId('cpu-usage')).findByText(
|
||||
'2 / 4 - 50%'
|
||||
)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it('should not display resource usage if user does not have K8sClusterNodeR authorization', async () => {
|
||||
mockUseAuthorizations.mockReturnValue({
|
||||
authorized: false,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
renderComponent();
|
||||
|
||||
// Should only show reservation bars
|
||||
expect(
|
||||
await within(await screen.findByTestId('memory-reservation')).findByText(
|
||||
'2147 / 8589 MB - 25%'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await within(await screen.findByTestId('cpu-reservation')).findByText(
|
||||
'1 / 4 - 25%'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
// Usage bars should not be present
|
||||
expect(screen.queryByTestId('memory-usage')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('cpu-usage')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display resource usage if metrics server is not enabled', async () => {
|
||||
const disabledMetricsEnvironment = createMockEnvironment();
|
||||
disabledMetricsEnvironment.Kubernetes.Configuration.UseServerMetrics =
|
||||
false;
|
||||
mockUseCurrentEnvironment.mockReturnValue(
|
||||
createMockQueryResult(disabledMetricsEnvironment)
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
// Should only show reservation bars
|
||||
expect(
|
||||
await within(await screen.findByTestId('memory-reservation')).findByText(
|
||||
'2147 / 8589 MB - 25%'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await within(await screen.findByTestId('cpu-reservation')).findByText(
|
||||
'1 / 4 - 25%'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
// Usage bars should not be present
|
||||
expect(screen.queryByTestId('memory-usage')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('cpu-usage')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display warning if metrics server is enabled but usage query fails', async () => {
|
||||
server.use(
|
||||
http.get('/api/kubernetes/3/metrics/nodes', () => HttpResponse.error())
|
||||
);
|
||||
|
||||
// Mock console.error so test logs are not polluted
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
renderComponent();
|
||||
|
||||
expect(
|
||||
await within(await screen.findByTestId('memory-reservation')).findByText(
|
||||
'2147 / 8589 MB - 25%'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await within(await screen.findByTestId('memory-usage')).findByText(
|
||||
'0 / 8589 MB - 0%'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await within(await screen.findByTestId('cpu-reservation')).findByText(
|
||||
'1 / 4 - 25%'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await within(await screen.findByTestId('cpu-usage')).findByText(
|
||||
'0 / 4 - 0%'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
// Should show the warning message
|
||||
expect(
|
||||
await screen.findByText(
|
||||
/Resource usage is not currently available as Metrics Server is not responding/
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
// Restore console.error
|
||||
vi.spyOn(console, 'error').mockRestore();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
import { Widget, WidgetBody } from '@/react/components/Widget';
|
||||
import { ResourceReservation } from '@/react/kubernetes/components/ResourceReservation';
|
||||
|
||||
import { useClusterResourceReservationData } from './useClusterResourceReservationData';
|
||||
|
||||
export function ClusterResourceReservation() {
|
||||
// Load all data required for this component
|
||||
const {
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
isLoading,
|
||||
displayResourceUsage,
|
||||
resourceUsage,
|
||||
resourceReservation,
|
||||
displayWarning,
|
||||
} = useClusterResourceReservationData();
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<WidgetBody>
|
||||
<ResourceReservation
|
||||
isLoading={isLoading}
|
||||
displayResourceUsage={displayResourceUsage}
|
||||
resourceReservation={resourceReservation}
|
||||
resourceUsage={resourceUsage}
|
||||
cpuLimit={cpuLimit}
|
||||
memoryLimit={memoryLimit}
|
||||
description="Resource reservation represents the total amount of resource assigned to all the applications inside the cluster."
|
||||
displayWarning={displayWarning}
|
||||
warningMessage="Resource usage is not currently available as Metrics Server is not responding. If you've recently upgraded, Metrics Server may take a while to restart, so please check back shortly."
|
||||
/>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
33
app/react/kubernetes/cluster/ClusterView/ClusterView.tsx
Normal file
33
app/react/kubernetes/cluster/ClusterView/ClusterView.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
||||
import { PageHeader } from '@/react/components/PageHeader';
|
||||
import { NodesDatatable } from '@/react/kubernetes/cluster/HomeView/NodesDatatable';
|
||||
|
||||
import { ClusterResourceReservation } from './ClusterResourceReservation';
|
||||
|
||||
export function ClusterView() {
|
||||
const { data: environment } = useCurrentEnvironment();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Cluster"
|
||||
breadcrumbs={[
|
||||
{ label: 'Environments', link: 'portainer.endpoints' },
|
||||
{
|
||||
label: environment?.Name || '',
|
||||
link: 'portainer.endpoints.endpoint',
|
||||
linkParams: { id: environment?.Id },
|
||||
},
|
||||
'Cluster information',
|
||||
]}
|
||||
reload
|
||||
/>
|
||||
|
||||
<ClusterResourceReservation />
|
||||
|
||||
<div className="row">
|
||||
<NodesDatatable />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
1
app/react/kubernetes/cluster/ClusterView/index.ts
Normal file
1
app/react/kubernetes/cluster/ClusterView/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { ClusterView } from './ClusterView';
|
|
@ -0,0 +1,3 @@
|
|||
export * from './useClusterResourceLimitsQuery';
|
||||
export * from './useClusterResourceReservationQuery';
|
||||
export * from './useClusterResourceUsageQuery';
|
|
@ -0,0 +1,49 @@
|
|||
import { round, reduce } from 'lodash';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Node } from 'kubernetes-types/core/v1';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
import KubernetesResourceReservationHelper from '@/kubernetes/helpers/resourceReservationHelper';
|
||||
import { parseCpu } from '@/react/kubernetes/utils';
|
||||
import { getNodes } from '@/react/kubernetes/cluster/HomeView/nodes.service';
|
||||
|
||||
export function useClusterResourceLimitsQuery(environmentId: EnvironmentId) {
|
||||
return useQuery(
|
||||
[environmentId, 'clusterResourceLimits'],
|
||||
async () => getNodes(environmentId),
|
||||
{
|
||||
...withGlobalError('Unable to retrieve resource limit data', 'Failure'),
|
||||
enabled: !!environmentId,
|
||||
select: aggregateResourceLimits,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes node data to calculate total CPU and memory limits for the cluster
|
||||
* and sets the state for memory limit in MB and CPU limit rounded to 3 decimal places.
|
||||
*/
|
||||
function aggregateResourceLimits(nodes: Node[]) {
|
||||
const processedNodes = nodes.map((node) => ({
|
||||
...node,
|
||||
memory: filesizeParser(node.status?.allocatable?.memory ?? ''),
|
||||
cpu: parseCpu(node.status?.allocatable?.cpu ?? ''),
|
||||
}));
|
||||
|
||||
return {
|
||||
nodes: processedNodes,
|
||||
memoryLimit: reduce(
|
||||
processedNodes,
|
||||
(acc, node) =>
|
||||
KubernetesResourceReservationHelper.megaBytesValue(node.memory || 0) +
|
||||
acc,
|
||||
0
|
||||
),
|
||||
cpuLimit: round(
|
||||
reduce(processedNodes, (acc, node) => (node.cpu || 0) + acc, 0),
|
||||
3
|
||||
),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Node } from 'kubernetes-types/core/v1';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { getTotalResourcesForAllApplications } from '@/react/kubernetes/metrics/metrics';
|
||||
import KubernetesResourceReservationHelper from '@/kubernetes/helpers/resourceReservationHelper';
|
||||
|
||||
export function useClusterResourceReservationQuery(
|
||||
environmentId: EnvironmentId,
|
||||
nodes: Node[]
|
||||
) {
|
||||
return useQuery(
|
||||
[environmentId, 'clusterResourceReservation'],
|
||||
() => getTotalResourcesForAllApplications(environmentId),
|
||||
{
|
||||
enabled: !!environmentId && nodes.length > 0,
|
||||
select: (data) => ({
|
||||
cpu: data.CpuRequest / 1000,
|
||||
memory: KubernetesResourceReservationHelper.megaBytesValue(
|
||||
data.MemoryRequest
|
||||
),
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Node } from 'kubernetes-types/core/v1';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { getMetricsForAllNodes } from '@/react/kubernetes/metrics/metrics';
|
||||
import KubernetesResourceReservationHelper from '@/kubernetes/helpers/resourceReservationHelper';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
import { NodeMetrics } from '@/react/kubernetes/metrics/types';
|
||||
|
||||
export function useClusterResourceUsageQuery(
|
||||
environmentId: EnvironmentId,
|
||||
serverMetricsEnabled: boolean,
|
||||
authorized: boolean,
|
||||
nodes: Node[]
|
||||
) {
|
||||
return useQuery(
|
||||
[environmentId, 'clusterResourceUsage'],
|
||||
() => getMetricsForAllNodes(environmentId),
|
||||
{
|
||||
enabled:
|
||||
authorized &&
|
||||
serverMetricsEnabled &&
|
||||
!!environmentId &&
|
||||
nodes.length > 0,
|
||||
select: aggregateResourceUsage,
|
||||
...withGlobalError('Unable to retrieve resource usage data.', 'Failure'),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function aggregateResourceUsage(data: NodeMetrics) {
|
||||
return data.items.reduce(
|
||||
(total, item) => ({
|
||||
cpu:
|
||||
total.cpu +
|
||||
KubernetesResourceReservationHelper.parseCPU(item.usage.cpu),
|
||||
memory:
|
||||
total.memory +
|
||||
KubernetesResourceReservationHelper.megaBytesValue(item.usage.memory),
|
||||
}),
|
||||
{
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { useAuthorizations } from '@/react/hooks/useUser';
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { getSafeValue } from '@/react/kubernetes/utils';
|
||||
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
||||
|
||||
import {
|
||||
useClusterResourceLimitsQuery,
|
||||
useClusterResourceReservationQuery,
|
||||
useClusterResourceUsageQuery,
|
||||
} from './queries';
|
||||
|
||||
export function useClusterResourceReservationData() {
|
||||
const { data: environment } = useCurrentEnvironment();
|
||||
const environmentId = useEnvironmentId();
|
||||
|
||||
// Check if server metrics is enabled
|
||||
const serverMetricsEnabled =
|
||||
environment?.Kubernetes?.Configuration?.UseServerMetrics || false;
|
||||
|
||||
// User needs to have K8sClusterNodeR authorization to view resource usage data
|
||||
const { authorized: hasK8sClusterNodeR } = useAuthorizations(
|
||||
['K8sClusterNodeR'],
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
||||
// Get resource limits for the cluster
|
||||
const { data: resourceLimits, isLoading: isResourceLimitLoading } =
|
||||
useClusterResourceLimitsQuery(environmentId);
|
||||
|
||||
// Get resource reservation info for the cluster
|
||||
const {
|
||||
data: resourceReservation,
|
||||
isFetching: isResourceReservationLoading,
|
||||
} = useClusterResourceReservationQuery(
|
||||
environmentId,
|
||||
resourceLimits?.nodes || []
|
||||
);
|
||||
|
||||
// Get resource usage info for the cluster
|
||||
const {
|
||||
data: resourceUsage,
|
||||
isFetching: isResourceUsageLoading,
|
||||
isError: isResourceUsageError,
|
||||
} = useClusterResourceUsageQuery(
|
||||
environmentId,
|
||||
serverMetricsEnabled,
|
||||
hasK8sClusterNodeR,
|
||||
resourceLimits?.nodes || []
|
||||
);
|
||||
|
||||
return {
|
||||
memoryLimit: getSafeValue(resourceLimits?.memoryLimit || 0),
|
||||
cpuLimit: getSafeValue(resourceLimits?.cpuLimit || 0),
|
||||
displayResourceUsage: hasK8sClusterNodeR && serverMetricsEnabled,
|
||||
resourceUsage: {
|
||||
cpu: getSafeValue(resourceUsage?.cpu || 0),
|
||||
memory: getSafeValue(resourceUsage?.memory || 0),
|
||||
},
|
||||
resourceReservation: {
|
||||
cpu: getSafeValue(resourceReservation?.cpu || 0),
|
||||
memory: getSafeValue(resourceReservation?.memory || 0),
|
||||
},
|
||||
isLoading:
|
||||
isResourceLimitLoading ||
|
||||
isResourceReservationLoading ||
|
||||
isResourceUsageLoading,
|
||||
// Display warning if server metrics isn't responding but should be
|
||||
displayWarning:
|
||||
hasK8sClusterNodeR && serverMetricsEnabled && isResourceUsageError,
|
||||
};
|
||||
}
|
|
@ -45,7 +45,7 @@ export function useNodeQuery(environmentId: EnvironmentId, nodeName: string) {
|
|||
}
|
||||
|
||||
// getNodes is used to get a list of nodes using the kubernetes API
|
||||
async function getNodes(environmentId: EnvironmentId) {
|
||||
export async function getNodes(environmentId: EnvironmentId) {
|
||||
try {
|
||||
const { data: nodeList } = await axios.get<NodeList>(
|
||||
`/endpoints/${environmentId}/kubernetes/api/v1/nodes`
|
||||
|
|
129
app/react/kubernetes/components/ResourceReservation.tsx
Normal file
129
app/react/kubernetes/components/ResourceReservation.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
import { round } from 'lodash';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
import { FormSectionTitle } from '@/react/components/form-components/FormSectionTitle';
|
||||
import { TextTip } from '@/react/components/Tip/TextTip';
|
||||
import { ResourceUsageItem } from '@/react/kubernetes/components/ResourceUsageItem';
|
||||
import { getPercentageString, getSafeValue } from '@/react/kubernetes/utils';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
interface ResourceMetrics {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
displayResourceUsage: boolean;
|
||||
resourceReservation: ResourceMetrics;
|
||||
resourceUsage: ResourceMetrics;
|
||||
cpuLimit: number;
|
||||
memoryLimit: number;
|
||||
description: string;
|
||||
isLoading?: boolean;
|
||||
title?: string;
|
||||
displayWarning?: boolean;
|
||||
warningMessage?: string;
|
||||
}
|
||||
|
||||
export function ResourceReservation({
|
||||
displayResourceUsage,
|
||||
resourceReservation,
|
||||
resourceUsage,
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
description,
|
||||
title = 'Resource reservation',
|
||||
isLoading = false,
|
||||
displayWarning = false,
|
||||
warningMessage = '',
|
||||
}: Props) {
|
||||
const memoryReservationAnnotation = `${getSafeValue(
|
||||
resourceReservation.memory
|
||||
)} / ${memoryLimit} MB ${getPercentageString(
|
||||
resourceReservation.memory,
|
||||
memoryLimit
|
||||
)}`;
|
||||
|
||||
const memoryUsageAnnotation = `${getSafeValue(
|
||||
resourceUsage.memory
|
||||
)} / ${memoryLimit} MB ${getPercentageString(
|
||||
resourceUsage.memory,
|
||||
memoryLimit
|
||||
)}`;
|
||||
|
||||
const cpuReservationAnnotation = `${round(
|
||||
getSafeValue(resourceReservation.cpu),
|
||||
2
|
||||
)} / ${round(getSafeValue(cpuLimit), 2)} ${getPercentageString(
|
||||
resourceReservation.cpu,
|
||||
cpuLimit
|
||||
)}`;
|
||||
|
||||
const cpuUsageAnnotation = `${round(
|
||||
getSafeValue(resourceUsage.cpu),
|
||||
2
|
||||
)} / ${round(getSafeValue(cpuLimit), 2)} ${getPercentageString(
|
||||
resourceUsage.cpu,
|
||||
cpuLimit
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormSectionTitle>{title}</FormSectionTitle>
|
||||
<TextTip color="blue" className="mb-2">
|
||||
{description}
|
||||
</TextTip>
|
||||
<div className="form-horizontal">
|
||||
{memoryLimit > 0 && (
|
||||
<ResourceUsageItem
|
||||
value={resourceReservation.memory}
|
||||
total={memoryLimit}
|
||||
label="Memory reservation"
|
||||
annotation={memoryReservationAnnotation}
|
||||
isLoading={isLoading}
|
||||
dataCy="memory-reservation"
|
||||
/>
|
||||
)}
|
||||
{displayResourceUsage && memoryLimit > 0 && (
|
||||
<ResourceUsageItem
|
||||
value={resourceUsage.memory}
|
||||
total={memoryLimit}
|
||||
label="Memory usage"
|
||||
annotation={memoryUsageAnnotation}
|
||||
isLoading={isLoading}
|
||||
dataCy="memory-usage"
|
||||
/>
|
||||
)}
|
||||
{cpuLimit > 0 && (
|
||||
<ResourceUsageItem
|
||||
value={resourceReservation.cpu}
|
||||
total={cpuLimit}
|
||||
label="CPU reservation"
|
||||
annotation={cpuReservationAnnotation}
|
||||
isLoading={isLoading}
|
||||
dataCy="cpu-reservation"
|
||||
/>
|
||||
)}
|
||||
{displayResourceUsage && cpuLimit > 0 && (
|
||||
<ResourceUsageItem
|
||||
value={resourceUsage.cpu}
|
||||
total={cpuLimit}
|
||||
label="CPU usage"
|
||||
annotation={cpuUsageAnnotation}
|
||||
isLoading={isLoading}
|
||||
dataCy="cpu-usage"
|
||||
/>
|
||||
)}
|
||||
{displayWarning && (
|
||||
<div className="form-group">
|
||||
<span className="col-sm-12 text-warning small vertical-center">
|
||||
<Icon icon={AlertTriangle} mode="warning" />
|
||||
{warningMessage}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
import { ProgressBar } from '@@/ProgressBar';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { ProgressBar } from '@@/ProgressBar';
|
||||
|
||||
interface ResourceUsageItemProps {
|
||||
value: number;
|
||||
total: number;
|
||||
annotation?: React.ReactNode;
|
||||
label: string;
|
||||
isLoading?: boolean;
|
||||
dataCy?: string;
|
||||
}
|
||||
|
||||
export function ResourceUsageItem({
|
||||
|
@ -13,9 +15,16 @@ export function ResourceUsageItem({
|
|||
total,
|
||||
annotation,
|
||||
label,
|
||||
isLoading = false,
|
||||
dataCy,
|
||||
}: ResourceUsageItemProps) {
|
||||
return (
|
||||
<FormControl label={label}>
|
||||
<FormControl
|
||||
label={label}
|
||||
isLoading={isLoading}
|
||||
className={isLoading ? 'mb-1.5' : ''}
|
||||
dataCy={dataCy}
|
||||
>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<ProgressBar
|
||||
steps={[
|
|
@ -37,8 +37,8 @@ export type Usage = {
|
|||
};
|
||||
|
||||
export type ApplicationResource = {
|
||||
cpuRequest: number;
|
||||
cpuLimit: number;
|
||||
memoryRequest: number;
|
||||
memoryLimit: number;
|
||||
CpuRequest: number;
|
||||
CpuLimit: number;
|
||||
MemoryRequest: number;
|
||||
MemoryLimit: number;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { ResourceReservation } from '@/react/kubernetes/components/ResourceReservation';
|
||||
|
||||
import { ResourceQuotaFormValues } from './types';
|
||||
import { useNamespaceResourceReservationData } from './useNamespaceResourceReservationData';
|
||||
|
||||
interface Props {
|
||||
namespaceName: string;
|
||||
environmentId: number;
|
||||
resourceQuotaValues: ResourceQuotaFormValues;
|
||||
}
|
||||
|
||||
export function NamespaceResourceReservation({
|
||||
environmentId,
|
||||
namespaceName,
|
||||
resourceQuotaValues,
|
||||
}: Props) {
|
||||
const {
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
displayResourceUsage,
|
||||
resourceUsage,
|
||||
resourceReservation,
|
||||
isLoading,
|
||||
} = useNamespaceResourceReservationData(
|
||||
environmentId,
|
||||
namespaceName,
|
||||
resourceQuotaValues
|
||||
);
|
||||
|
||||
if (!resourceQuotaValues.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceReservation
|
||||
displayResourceUsage={displayResourceUsage}
|
||||
resourceReservation={resourceReservation}
|
||||
resourceUsage={resourceUsage}
|
||||
cpuLimit={cpuLimit}
|
||||
memoryLimit={memoryLimit}
|
||||
description="Resource reservation represents the total amount of resource assigned to all the applications deployed inside this namespace."
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -13,8 +13,8 @@ import { SliderWithInput } from '@@/form-components/Slider/SliderWithInput';
|
|||
|
||||
import { useClusterResourceLimitsQuery } from '../../../queries/useResourceLimitsQuery';
|
||||
|
||||
import { ResourceReservationUsage } from './ResourceReservationUsage';
|
||||
import { ResourceQuotaFormValues } from './types';
|
||||
import { NamespaceResourceReservation } from './NamespaceResourceReservation';
|
||||
|
||||
interface Props {
|
||||
values: ResourceQuotaFormValues;
|
||||
|
@ -128,7 +128,7 @@ export function ResourceQuotaFormSection({
|
|||
</div>
|
||||
)}
|
||||
{namespaceName && isEdit && (
|
||||
<ResourceReservationUsage
|
||||
<NamespaceResourceReservation
|
||||
namespaceName={namespaceName}
|
||||
environmentId={environmentId}
|
||||
resourceQuotaValues={values}
|
||||
|
|
|
@ -1,150 +0,0 @@
|
|||
import { round } from 'lodash';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { useMetricsForNamespace } from '@/react/kubernetes/metrics/queries/useMetricsForNamespace';
|
||||
import { PodMetrics } from '@/react/kubernetes/metrics/types';
|
||||
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
||||
|
||||
import { megaBytesValue, parseCPU } from '../../../resourceQuotaUtils';
|
||||
import { ResourceUsageItem } from '../../ResourceUsageItem';
|
||||
|
||||
import { useResourceQuotaUsed } from './useResourceQuotaUsed';
|
||||
import { ResourceQuotaFormValues } from './types';
|
||||
|
||||
export function ResourceReservationUsage({
|
||||
namespaceName,
|
||||
environmentId,
|
||||
resourceQuotaValues,
|
||||
}: {
|
||||
namespaceName: string;
|
||||
environmentId: EnvironmentId;
|
||||
resourceQuotaValues: ResourceQuotaFormValues;
|
||||
}) {
|
||||
const namespaceMetricsQuery = useMetricsForNamespace(
|
||||
environmentId,
|
||||
namespaceName,
|
||||
{
|
||||
select: aggregatePodUsage,
|
||||
}
|
||||
);
|
||||
const usedResourceQuotaQuery = useResourceQuotaUsed(
|
||||
environmentId,
|
||||
namespaceName
|
||||
);
|
||||
const { data: namespaceMetrics } = namespaceMetricsQuery;
|
||||
const { data: usedResourceQuota } = usedResourceQuotaQuery;
|
||||
|
||||
const memoryQuota = Number(resourceQuotaValues.memory) ?? 0;
|
||||
const cpuQuota = Number(resourceQuotaValues.cpu) ?? 0;
|
||||
|
||||
if (!resourceQuotaValues.enabled) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<FormSectionTitle>Resource reservation</FormSectionTitle>
|
||||
<TextTip color="blue" className="mb-2">
|
||||
Resource reservation represents the total amount of resource assigned to
|
||||
all the applications deployed inside this namespace.
|
||||
</TextTip>
|
||||
{!!usedResourceQuota && memoryQuota > 0 && (
|
||||
<ResourceUsageItem
|
||||
value={usedResourceQuota.memory}
|
||||
total={getSafeValue(memoryQuota)}
|
||||
label="Memory reservation"
|
||||
annotation={`${usedResourceQuota.memory} / ${getSafeValue(
|
||||
memoryQuota
|
||||
)} MB ${getPercentageString(usedResourceQuota.memory, memoryQuota)}`}
|
||||
/>
|
||||
)}
|
||||
{!!namespaceMetrics && memoryQuota > 0 && (
|
||||
<ResourceUsageItem
|
||||
value={namespaceMetrics.memory}
|
||||
total={getSafeValue(memoryQuota)}
|
||||
label="Memory used"
|
||||
annotation={`${namespaceMetrics.memory} / ${getSafeValue(
|
||||
memoryQuota
|
||||
)} MB ${getPercentageString(namespaceMetrics.memory, memoryQuota)}`}
|
||||
/>
|
||||
)}
|
||||
{!!usedResourceQuota && cpuQuota > 0 && (
|
||||
<ResourceUsageItem
|
||||
value={usedResourceQuota.cpu}
|
||||
total={cpuQuota}
|
||||
label="CPU reservation"
|
||||
annotation={`${
|
||||
usedResourceQuota.cpu
|
||||
} / ${cpuQuota} ${getPercentageString(
|
||||
usedResourceQuota.cpu,
|
||||
cpuQuota
|
||||
)}`}
|
||||
/>
|
||||
)}
|
||||
{!!namespaceMetrics && cpuQuota > 0 && (
|
||||
<ResourceUsageItem
|
||||
value={namespaceMetrics.cpu}
|
||||
total={cpuQuota}
|
||||
label="CPU used"
|
||||
annotation={`${
|
||||
namespaceMetrics.cpu
|
||||
} / ${cpuQuota} ${getPercentageString(
|
||||
namespaceMetrics.cpu,
|
||||
cpuQuota
|
||||
)}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getSafeValue(value: number | string) {
|
||||
const valueNumber = Number(value);
|
||||
if (Number.isNaN(valueNumber)) {
|
||||
return 0;
|
||||
}
|
||||
return valueNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the percentage of the value over the total.
|
||||
* @param value - The value to calculate the percentage for.
|
||||
* @param total - The total value to compare the percentage to.
|
||||
* @returns The percentage of the value over the total, with the '- ' string prefixed, for example '- 50%'.
|
||||
*/
|
||||
function getPercentageString(value: number, total?: number | string) {
|
||||
const totalNumber = Number(total);
|
||||
if (
|
||||
totalNumber === 0 ||
|
||||
total === undefined ||
|
||||
total === '' ||
|
||||
Number.isNaN(totalNumber)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
if (value > totalNumber) {
|
||||
return '- Exceeded';
|
||||
}
|
||||
return `- ${Math.round((value / totalNumber) * 100)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates the resource usage of all the containers in the namespace.
|
||||
* @param podMetricsList - List of pod metrics
|
||||
* @returns Aggregated resource usage. CPU cores are rounded to 3 decimal places. Memory is in MB.
|
||||
*/
|
||||
function aggregatePodUsage(podMetricsList: PodMetrics) {
|
||||
const containerResourceUsageList = podMetricsList.items.flatMap((i) =>
|
||||
i.containers.map((c) => c.usage)
|
||||
);
|
||||
const namespaceResourceUsage = containerResourceUsageList.reduce(
|
||||
(total, usage) => ({
|
||||
cpu: total.cpu + parseCPU(usage.cpu),
|
||||
memory: total.memory + megaBytesValue(usage.memory),
|
||||
}),
|
||||
{ cpu: 0, memory: 0 }
|
||||
);
|
||||
namespaceResourceUsage.cpu = round(namespaceResourceUsage.cpu, 3);
|
||||
return namespaceResourceUsage;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import { round } from 'lodash';
|
||||
|
||||
import { getSafeValue } from '@/react/kubernetes/utils';
|
||||
import { PodMetrics } from '@/react/kubernetes/metrics/types';
|
||||
import { useMetricsForNamespace } from '@/react/kubernetes/metrics/queries/useMetricsForNamespace';
|
||||
import {
|
||||
megaBytesValue,
|
||||
parseCPU,
|
||||
} from '@/react/kubernetes/namespaces/resourceQuotaUtils';
|
||||
|
||||
import { useResourceQuotaUsed } from './useResourceQuotaUsed';
|
||||
import { ResourceQuotaFormValues } from './types';
|
||||
|
||||
export function useNamespaceResourceReservationData(
|
||||
environmentId: number,
|
||||
namespaceName: string,
|
||||
resourceQuotaValues: ResourceQuotaFormValues
|
||||
) {
|
||||
const { data: quota, isLoading: isQuotaLoading } = useResourceQuotaUsed(
|
||||
environmentId,
|
||||
namespaceName
|
||||
);
|
||||
const { data: metrics, isLoading: isMetricsLoading } = useMetricsForNamespace(
|
||||
environmentId,
|
||||
namespaceName,
|
||||
{
|
||||
select: aggregatePodUsage,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
cpuLimit: Number(resourceQuotaValues.cpu) || 0,
|
||||
memoryLimit: Number(resourceQuotaValues.memory) || 0,
|
||||
displayResourceUsage: !!metrics,
|
||||
resourceReservation: {
|
||||
cpu: getSafeValue(quota?.cpu || 0),
|
||||
memory: getSafeValue(quota?.memory || 0),
|
||||
},
|
||||
resourceUsage: {
|
||||
cpu: getSafeValue(metrics?.cpu || 0),
|
||||
memory: getSafeValue(metrics?.memory || 0),
|
||||
},
|
||||
isLoading: isQuotaLoading || isMetricsLoading,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates the resource usage of all the containers in the namespace.
|
||||
* @param podMetricsList - List of pod metrics
|
||||
* @returns Aggregated resource usage. CPU cores are rounded to 3 decimal places. Memory is in MB.
|
||||
*/
|
||||
function aggregatePodUsage(podMetricsList: PodMetrics) {
|
||||
const containerResourceUsageList = podMetricsList.items.flatMap((i) =>
|
||||
i.containers.map((c) => c.usage)
|
||||
);
|
||||
const namespaceResourceUsage = containerResourceUsageList.reduce(
|
||||
(total, usage) => ({
|
||||
cpu: total.cpu + parseCPU(usage.cpu),
|
||||
memory: total.memory + megaBytesValue(usage.memory),
|
||||
}),
|
||||
{ cpu: 0, memory: 0 }
|
||||
);
|
||||
namespaceResourceUsage.cpu = round(namespaceResourceUsage.cpu, 3);
|
||||
return namespaceResourceUsage;
|
||||
}
|
|
@ -20,3 +20,38 @@ export function prepareAnnotations(annotations?: Annotation[]) {
|
|||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the safe value of the given number or string.
|
||||
* @param value - The value to get the safe value for.
|
||||
* @returns The safe value of the given number or string.
|
||||
*/
|
||||
export function getSafeValue(value: number | string) {
|
||||
const valueNumber = Number(value);
|
||||
if (Number.isNaN(valueNumber)) {
|
||||
return 0;
|
||||
}
|
||||
return valueNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the percentage of the value over the total.
|
||||
* @param value - The value to calculate the percentage for.
|
||||
* @param total - The total value to compare the percentage to.
|
||||
* @returns The percentage of the value over the total, with the '- ' string prefixed, for example '- 50%'.
|
||||
*/
|
||||
export function getPercentageString(value: number, total?: number | string) {
|
||||
const totalNumber = Number(total);
|
||||
if (
|
||||
totalNumber === 0 ||
|
||||
total === undefined ||
|
||||
total === '' ||
|
||||
Number.isNaN(totalNumber)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
if (value > totalNumber) {
|
||||
return '- Exceeded';
|
||||
}
|
||||
return `- ${Math.round((value / totalNumber) * 100)}%`;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue