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} >