diff --git a/app/kubernetes/react/views/index.ts b/app/kubernetes/react/views/index.ts
index a6c440de3..7913f7641 100644
--- a/app/kubernetes/react/views/index.ts
+++ b/app/kubernetes/react/views/index.ts
@@ -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))), [])
diff --git a/app/kubernetes/views/cluster/cluster.html b/app/kubernetes/views/cluster/cluster.html
deleted file mode 100644
index 46acbf931..000000000
--- a/app/kubernetes/views/cluster/cluster.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
diff --git a/app/kubernetes/views/cluster/cluster.js b/app/kubernetes/views/cluster/cluster.js
deleted file mode 100644
index 708076037..000000000
--- a/app/kubernetes/views/cluster/cluster.js
+++ /dev/null
@@ -1,8 +0,0 @@
-angular.module('portainer.kubernetes').component('kubernetesClusterView', {
- templateUrl: './cluster.html',
- controller: 'KubernetesClusterController',
- controllerAs: 'ctrl',
- bindings: {
- endpoint: '<',
- },
-});
diff --git a/app/kubernetes/views/cluster/clusterController.js b/app/kubernetes/views/cluster/clusterController.js
deleted file mode 100644
index b8f6df290..000000000
--- a/app/kubernetes/views/cluster/clusterController.js
+++ /dev/null
@@ -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);
diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts
index 556d7e4c2..20fe7dee3 100644
--- a/app/react-tools/test-mocks.ts
+++ b/app/react-tools/test-mocks.ts
@@ -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(
+ data: TData,
+ overrides?: Partial>
+) {
+ 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 };
+}
diff --git a/app/react/components/form-components/FormControl/FormControl.tsx b/app/react/components/form-components/FormControl/FormControl.tsx
index 5f4c8727a..7c7151dd6 100644
--- a/app/react/components/form-components/FormControl/FormControl.tsx
+++ b/app/react/components/form-components/FormControl/FormControl.tsx
@@ -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['message'];
- setTooltipHtmlMessage?: ComponentProps['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}
>