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
|
@ -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,
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue