From 959c527be7b0c6f3d8cd1c4eece373f6dfc7bcb0 Mon Sep 17 00:00:00 2001
From: Ali <83188384+testA113@users.noreply.github.com>
Date: Fri, 25 Oct 2024 12:28:05 +1300
Subject: [PATCH] refactor(apps): migrate applications view to react [r8s-124]
(#28)
---
app/kubernetes/__module.js | 4 +-
.../react/components/applications.ts | 21 -
app/kubernetes/react/components/index.ts | 12 -
app/kubernetes/react/views/index.ts | 5 +
.../views/applications/applications.html | 59 ---
.../views/applications/applications.js | 9 -
.../applications/applicationsController.js | 181 -------
.../AppYAMLEditor/useApplicationYAML.ts | 12 +-
.../ApplicationContainersDatatable.tsx | 3 +-
.../ApplicationAutoScalingTable.tsx | 2 +-
.../ApplicationDetailsWidget.tsx | 6 +-
.../ApplicationPersistentDataTable.tsx | 2 +-
.../RedeployApplicationButton.tsx | 2 +-
.../RollbackApplicationButton.tsx | 6 +-
.../ApplicationEventsDatatable.tsx | 8 +-
.../DetailsView/ApplicationSummaryWidget.tsx | 6 +-
.../usePlacementTableData.tsx | 3 +-
.../useApplicationEventsTableData.tsx | 8 +-
.../ApplicationsDatatable.tsx | 86 ++--
.../ListView/ApplicationsDatatable/index.ts | 1 +
.../ApplicationsStacksDatatable.tsx | 83 ++--
.../NamespaceFilter.tsx | 11 +-
.../TableActions.tsx | 24 -
.../ApplicationsStacksDatatable/types.ts | 7 -
.../ListView/ApplicationsView.tsx | 54 +++
.../ListView/useKubeAppsTableStore.ts | 55 +++
.../applications/application.queries.ts | 451 ------------------
.../applications/application.service.ts | 312 ------------
.../kubernetes/applications/pod.service.ts | 72 ---
.../applications/queries/getPods.ts | 37 ++
.../applications/queries/query-keys.ts | 105 ++++
.../applications/queries/useApplication.ts | 168 +++++++
.../useApplicationHorizontalPodAutoscaler.ts | 76 +++
.../queries/useApplicationPods.ts | 44 ++
.../queries/useApplicationRevisionList.ts | 173 +++++++
.../queries/useApplicationServices.ts | 57 +++
.../applications/queries/useApplications.ts | 54 +++
.../queries/useDeleteApplicationsMutation.ts | 191 ++++++++
.../useHorizontalPodAutoScaler.ts} | 34 +-
.../queries/usePatchApplicationMutation.ts | 155 ++++++
.../queries/useRedeployApplicationMutation.ts | 70 +++
.../NodeApplicationsDatatable.tsx | 2 +-
42 files changed, 1378 insertions(+), 1293 deletions(-)
delete mode 100644 app/kubernetes/react/components/applications.ts
delete mode 100644 app/kubernetes/views/applications/applications.html
delete mode 100644 app/kubernetes/views/applications/applications.js
delete mode 100644 app/kubernetes/views/applications/applicationsController.js
create mode 100644 app/react/kubernetes/applications/ListView/ApplicationsDatatable/index.ts
delete mode 100644 app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/TableActions.tsx
create mode 100644 app/react/kubernetes/applications/ListView/ApplicationsView.tsx
create mode 100644 app/react/kubernetes/applications/ListView/useKubeAppsTableStore.ts
delete mode 100644 app/react/kubernetes/applications/application.queries.ts
delete mode 100644 app/react/kubernetes/applications/application.service.ts
delete mode 100644 app/react/kubernetes/applications/pod.service.ts
create mode 100644 app/react/kubernetes/applications/queries/getPods.ts
create mode 100644 app/react/kubernetes/applications/queries/query-keys.ts
create mode 100644 app/react/kubernetes/applications/queries/useApplication.ts
create mode 100644 app/react/kubernetes/applications/queries/useApplicationHorizontalPodAutoscaler.ts
create mode 100644 app/react/kubernetes/applications/queries/useApplicationPods.ts
create mode 100644 app/react/kubernetes/applications/queries/useApplicationRevisionList.ts
create mode 100644 app/react/kubernetes/applications/queries/useApplicationServices.ts
create mode 100644 app/react/kubernetes/applications/queries/useApplications.ts
create mode 100644 app/react/kubernetes/applications/queries/useDeleteApplicationsMutation.ts
rename app/react/kubernetes/applications/{autoscaling.service.ts => queries/useHorizontalPodAutoScaler.ts} (64%)
create mode 100644 app/react/kubernetes/applications/queries/usePatchApplicationMutation.ts
create mode 100644 app/react/kubernetes/applications/queries/useRedeployApplicationMutation.ts
diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js
index 1eaee62ad..2528e67bc 100644
--- a/app/kubernetes/__module.js
+++ b/app/kubernetes/__module.js
@@ -207,7 +207,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
const applications = {
name: 'kubernetes.applications',
- url: '/applications',
+ url: '/applications?tab',
views: {
'content@': {
component: 'kubernetesApplicationsView',
@@ -233,7 +233,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
const application = {
name: 'kubernetes.applications.application',
- url: '/:namespace/:name?resource-type&tab',
+ url: '/:namespace/:name?resource-type',
views: {
'content@': {
component: 'applicationDetailsView',
diff --git a/app/kubernetes/react/components/applications.ts b/app/kubernetes/react/components/applications.ts
deleted file mode 100644
index 844ab6a92..000000000
--- a/app/kubernetes/react/components/applications.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import angular from 'angular';
-
-import { r2a } from '@/react-tools/react2angular';
-import { withUIRouter } from '@/react-tools/withUIRouter';
-import { withCurrentUser } from '@/react-tools/withCurrentUser';
-import { ApplicationsDatatable } from '@/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable';
-
-export const applicationsModule = angular
- .module('portainer.kubernetes.react.components.applications', [])
-
- .component(
- 'kubernetesApplicationsDatatable',
- r2a(withUIRouter(withCurrentUser(ApplicationsDatatable)), [
- 'namespace',
- 'namespaces',
- 'onNamespaceChange',
- 'onRefresh',
- 'onRemove',
- 'hideStacks',
- ])
- ).name;
diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts
index 081b5fbce..e42874d86 100644
--- a/app/kubernetes/react/components/index.ts
+++ b/app/kubernetes/react/components/index.ts
@@ -23,7 +23,6 @@ import { ApplicationSummarySection } from '@/react/kubernetes/applications/compo
import { withFormValidation } from '@/react-tools/withFormValidation';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { YAMLInspector } from '@/react/kubernetes/components/YAMLInspector';
-import { ApplicationsStacksDatatable } from '@/react/kubernetes/applications/ListView/ApplicationsStacksDatatable';
import { NodesDatatable } from '@/react/kubernetes/cluster/HomeView/NodesDatatable';
import { StackName } from '@/react/kubernetes/DeployView/StackName/StackName';
import { StackNameLabelInsight } from '@/react/kubernetes/DeployView/StackName/StackNameLabelInsight';
@@ -61,14 +60,12 @@ import { EnvironmentVariablesFormSection } from '@/react/kubernetes/applications
import { kubeEnvVarValidationSchema } from '@/react/kubernetes/applications/components/EnvironmentVariablesFormSection/kubeEnvVarValidationSchema';
import { IntegratedAppsDatatable } from '@/react/kubernetes/components/IntegratedAppsDatatable/IntegratedAppsDatatable';
-import { applicationsModule } from './applications';
import { namespacesModule } from './namespaces';
import { clusterManagementModule } from './clusterManagement';
import { registriesModule } from './registries';
export const ngModule = angular
.module('portainer.kubernetes.react.components', [
- applicationsModule,
namespacesModule,
clusterManagementModule,
registriesModule,
@@ -208,15 +205,6 @@ export const ngModule = angular
['formValues', 'oldFormValues']
)
)
- .component(
- 'kubernetesApplicationsStacksDatatable',
- r2a(withUIRouter(withCurrentUser(ApplicationsStacksDatatable)), [
- 'onRemove',
- 'namespace',
- 'namespaces',
- 'onNamespaceChange',
- ])
- )
.component(
'kubernetesIntegratedApplicationsDatatable',
r2a(withUIRouter(withCurrentUser(IntegratedAppsDatatable)), [
diff --git a/app/kubernetes/react/views/index.ts b/app/kubernetes/react/views/index.ts
index 2deae5fb5..8ee8538e3 100644
--- a/app/kubernetes/react/views/index.ts
+++ b/app/kubernetes/react/views/index.ts
@@ -11,6 +11,7 @@ import { ServicesView } from '@/react/kubernetes/services/ServicesView';
import { ConsoleView } from '@/react/kubernetes/applications/ConsoleView';
import { ConfigmapsAndSecretsView } from '@/react/kubernetes/configs/ListView/ConfigmapsAndSecretsView';
import { CreateNamespaceView } from '@/react/kubernetes/namespaces/CreateView/CreateNamespaceView';
+import { ApplicationsView } from '@/react/kubernetes/applications/ListView/ApplicationsView';
import { ApplicationDetailsView } from '@/react/kubernetes/applications/DetailsView/ApplicationDetailsView';
import { ConfigureView } from '@/react/kubernetes/cluster/ConfigureView';
import { NamespacesView } from '@/react/kubernetes/namespaces/ListView/NamespacesView';
@@ -55,6 +56,10 @@ export const viewsModule = angular
[]
)
)
+ .component(
+ 'kubernetesApplicationsView',
+ r2a(withUIRouter(withReactQuery(withCurrentUser(ApplicationsView))), [])
+ )
.component(
'applicationDetailsView',
r2a(
diff --git a/app/kubernetes/views/applications/applications.html b/app/kubernetes/views/applications/applications.html
deleted file mode 100644
index 44fc20be8..000000000
--- a/app/kubernetes/views/applications/applications.html
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- Applications
-
-
-
-
-
- Stacks
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/kubernetes/views/applications/applications.js b/app/kubernetes/views/applications/applications.js
deleted file mode 100644
index da0a7ff07..000000000
--- a/app/kubernetes/views/applications/applications.js
+++ /dev/null
@@ -1,9 +0,0 @@
-angular.module('portainer.kubernetes').component('kubernetesApplicationsView', {
- templateUrl: './applications.html',
- controller: 'KubernetesApplicationsController',
- controllerAs: 'ctrl',
- bindings: {
- $transition$: '<',
- endpoint: '<',
- },
-});
diff --git a/app/kubernetes/views/applications/applicationsController.js b/app/kubernetes/views/applications/applicationsController.js
deleted file mode 100644
index e7fb8d431..000000000
--- a/app/kubernetes/views/applications/applicationsController.js
+++ /dev/null
@@ -1,181 +0,0 @@
-import angular from 'angular';
-import _ from 'lodash-es';
-import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
-import { KubernetesPortainerApplicationStackNameLabel } from 'Kubernetes/models/application/models';
-import { getDeploymentOptions } from '@/react/portainer/environments/environment.service';
-import { getStacksFromApplications } from '@/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/getStacksFromApplications';
-import { getApplications } from '@/react/kubernetes/applications/application.queries.ts';
-import { getNamespaces } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
-class KubernetesApplicationsController {
- /* @ngInject */
- constructor(
- $async,
- $state,
- $scope,
- Authentication,
- Notifications,
- KubernetesApplicationService,
- EndpointService,
- HelmService,
- KubernetesConfigurationService,
- LocalStorage,
- StackService,
- KubernetesNamespaceService
- ) {
- this.$async = $async;
- this.$state = $state;
- this.$scope = $scope;
- this.Authentication = Authentication;
- this.Notifications = Notifications;
- this.KubernetesApplicationService = KubernetesApplicationService;
- this.HelmService = HelmService;
- this.KubernetesConfigurationService = KubernetesConfigurationService;
- this.Authentication = Authentication;
- this.LocalStorage = LocalStorage;
- this.StackService = StackService;
- this.KubernetesNamespaceService = KubernetesNamespaceService;
-
- this.onInit = this.onInit.bind(this);
- this.removeAction = this.removeAction.bind(this);
- this.removeActionAsync = this.removeActionAsync.bind(this);
- this.removeStacksAction = this.removeStacksAction.bind(this);
- this.removeStacksActionAsync = this.removeStacksActionAsync.bind(this);
- this.onPublishingModeClick = this.onPublishingModeClick.bind(this);
- this.onChangeNamespaceDropdown = this.onChangeNamespaceDropdown.bind(this);
- }
-
- selectTab(index) {
- this.LocalStorage.storeActiveTab('applications', index);
- }
-
- async removeStacksActionAsync(selectedItems) {
- let actionCount = selectedItems.length;
- for (const stack of selectedItems) {
- try {
- const isAppFormCreated = stack.Applications.some((x) => !x.ApplicationKind);
-
- if (isAppFormCreated) {
- const promises = _.map(stack.Applications, (app) => this.KubernetesApplicationService.delete(app));
- await Promise.all(promises);
- }
-
- await this.StackService.removeKubernetesStacksByName(stack.Name, stack.ResourcePool, false, this.endpoint.Id);
-
- this.Notifications.success('Stack successfully removed', stack.Name);
- _.remove(this.state.stacks, { Name: stack.Name });
- } catch (err) {
- this.Notifications.error('Failure', err, 'Unable to remove stack');
- } finally {
- --actionCount;
- if (actionCount === 0) {
- this.$state.reload(this.$state.current);
- }
- }
- }
- }
-
- removeStacksAction(selectedItems) {
- return this.$async(this.removeStacksActionAsync, selectedItems);
- }
-
- async removeActionAsync(selectedItems) {
- let actionCount = selectedItems.length;
- for (const application of selectedItems) {
- try {
- if (application.ApplicationType === KubernetesApplicationTypes.Helm) {
- await this.HelmService.uninstall(this.endpoint.Id, application);
- } else {
- if (application.Metadata.labels && application.Metadata.labels[KubernetesPortainerApplicationStackNameLabel]) {
- // remove stack if no app left in the stack
- const appsInNamespace = await getApplications(this.endpoint.Id, { namespace: application.ResourcePool, withDependencies: false });
- const stacksInNamespace = getStacksFromApplications(appsInNamespace);
- const stack = stacksInNamespace.find((x) => x.Name === application.StackName);
- if (stack.Applications.length === 0 && application.StackId) {
- await this.StackService.remove({ Id: application.StackId }, false, this.endpoint.Id);
- }
- }
- await this.KubernetesApplicationService.delete(application);
- }
- this.Notifications.success('Application successfully removed', application.Name);
- } catch (err) {
- this.Notifications.error('Failure', err, 'Unable to remove application');
- } finally {
- --actionCount;
- if (actionCount === 0) {
- this.$state.reload(this.$state.current);
- }
- }
- }
- }
-
- removeAction(selectedItems) {
- this.$async(() => this.removeActionAsync(selectedItems));
- }
-
- onPublishingModeClick(application) {
- this.state.activeTab = 1;
- _.forEach(this.state.ports, (item) => {
- item.Expanded = false;
- item.Highlighted = false;
- if (item.Name === application.Name && item.Ports.length > 1) {
- item.Expanded = true;
- item.Highlighted = true;
- }
- });
- }
-
- onChangeNamespaceDropdown(namespaceName) {
- return this.$async(async () => {
- this.state.namespaceName = namespaceName;
- // save the selected namespaceName in local storage with the key 'kubernetes_namespace_filter_${environmentId}_${userID}'
- this.LocalStorage.storeNamespaceFilter(this.endpoint.Id, this.user.ID, namespaceName);
- });
- }
-
- async onInit() {
- this.state = {
- activeTab: this.LocalStorage.getActiveTab('applications'),
- currentName: this.$state.$current.name,
- isAdmin: this.Authentication.isAdmin(),
- viewReady: false,
- applications: [],
- stacks: [],
- ports: [],
- namespaces: [],
- namespaceName: '',
- };
-
- this.deploymentOptions = await getDeploymentOptions();
-
- this.user = this.Authentication.getUserDetails();
- this.state.namespaces = await getNamespaces(this.endpoint.Id);
-
- const savedNamespace = this.LocalStorage.getNamespaceFilter(this.endpoint.Id, this.user.ID); // could be null if not found, and '' if all namepsaces is selected
- const preferredNamespace = savedNamespace === null ? 'default' : savedNamespace;
-
- this.state.namespaces = this.state.namespaces.filter((n) => n.Status.phase === 'Active');
- this.state.namespaces = _.sortBy(this.state.namespaces, 'Name');
- // set all namespaces ('') if there are no namespaces, or if all namespaces is selected
- if (!this.state.namespaces.length || preferredNamespace === '') {
- this.state.namespaceName = '';
- } else {
- // otherwise, set the preferred namespaceName if it exists, otherwise set the first namespaceName
- this.state.namespaceName = this.state.namespaces.find((n) => n.Name === preferredNamespace) ? preferredNamespace : this.state.namespaces[0].Name;
- }
-
- this.state.viewReady = true;
- }
-
- $onInit() {
- return this.$async(this.onInit);
- }
-
- $onDestroy() {
- if (this.state.currentName !== this.$state.$current.name) {
- this.LocalStorage.storeActiveTab('applications', 0);
- }
- }
-}
-
-export default KubernetesApplicationsController;
-angular.module('portainer.kubernetes').controller('KubernetesApplicationsController', KubernetesApplicationsController);
diff --git a/app/react/kubernetes/applications/DetailsView/AppYAMLEditor/useApplicationYAML.ts b/app/react/kubernetes/applications/DetailsView/AppYAMLEditor/useApplicationYAML.ts
index 8016e5ff6..f8cadbb2c 100644
--- a/app/react/kubernetes/applications/DetailsView/AppYAMLEditor/useApplicationYAML.ts
+++ b/app/react/kubernetes/applications/DetailsView/AppYAMLEditor/useApplicationYAML.ts
@@ -3,12 +3,10 @@ import { useMemo } from 'react';
import { useServicesQuery } from '@/react/kubernetes/services/service';
-import {
- useApplication,
- useApplicationHorizontalPodAutoscaler,
- useApplicationServices,
-} from '../../application.queries';
-import { useHorizontalAutoScalarQuery } from '../../autoscaling.service';
+import { useHorizontalPodAutoScaler } from '../../queries/useHorizontalPodAutoScaler';
+import { useApplication } from '../../queries/useApplication';
+import { useApplicationServices } from '../../queries/useApplicationServices';
+import { useApplicationHorizontalPodAutoscaler } from '../../queries/useApplicationHorizontalPodAutoscaler';
export function useApplicationYAML() {
const {
@@ -56,7 +54,7 @@ export function useApplicationYAML() {
application
);
const { data: autoScalarYAML, ...autoScalarYAMLQuery } =
- useHorizontalAutoScalarQuery(
+ useHorizontalPodAutoScaler(
environmentId,
namespace,
autoScalar?.metadata?.name || '',
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx
index a0d05dbad..b365c5851 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx
@@ -11,7 +11,8 @@ import { useEnvironment } from '@/react/portainer/environments/queries';
import { Datatable } from '@@/datatables';
import { useTableState } from '@@/datatables/useTableState';
-import { useApplication, useApplicationPods } from '../../application.queries';
+import { useApplication } from '../../queries/useApplication';
+import { useApplicationPods } from '../../queries/useApplicationPods';
import { ContainerRowData } from './types';
import { getColumns } from './columns';
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationAutoScalingTable.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationAutoScalingTable.tsx
index 798140bd1..b51237dcf 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationAutoScalingTable.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationAutoScalingTable.tsx
@@ -7,7 +7,7 @@ import { TextTip } from '@@/Tip/TextTip';
import { Tooltip } from '@@/Tip/Tooltip';
import { Application } from '../../types';
-import { useApplicationHorizontalPodAutoscaler } from '../../application.queries';
+import { useApplicationHorizontalPodAutoscaler } from '../../queries/useApplicationHorizontalPodAutoscaler';
type Props = {
environmentId: EnvironmentId;
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationDetailsWidget.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationDetailsWidget.tsx
index b845ee8da..f905fd843 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationDetailsWidget.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationDetailsWidget.tsx
@@ -11,12 +11,10 @@ import { AddButton, Button } from '@@/buttons';
import { Link } from '@@/Link';
import { Icon } from '@@/Icon';
-import {
- useApplication,
- useApplicationServices,
-} from '../../application.queries';
import { applicationIsKind, isExternalApplication } from '../../utils';
import { appStackIdLabel } from '../../constants';
+import { useApplication } from '../../queries/useApplication';
+import { useApplicationServices } from '../../queries/useApplicationServices';
import { RestartApplicationButton } from './RestartApplicationButton';
import { RedeployApplicationButton } from './RedeployApplicationButton';
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationPersistentDataTable.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationPersistentDataTable.tsx
index b77e40171..af63febeb 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationPersistentDataTable.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationPersistentDataTable.tsx
@@ -12,7 +12,7 @@ import { Link } from '@@/Link';
import { Application } from '../../types';
import { applicationIsKind } from '../../utils';
-import { useApplicationPods } from '../../application.queries';
+import { useApplicationPods } from '../../queries/useApplicationPods';
type Props = {
environmentId: EnvironmentId;
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/RedeployApplicationButton.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/RedeployApplicationButton.tsx
index 93d413e70..1ea80a4dc 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/RedeployApplicationButton.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/RedeployApplicationButton.tsx
@@ -12,12 +12,12 @@ import { buildConfirmButton } from '@@/modals/utils';
import { Button } from '@@/buttons';
import { Icon } from '@@/Icon';
-import { useRedeployApplicationMutation } from '../../application.queries';
import { Application } from '../../types';
import {
applicationIsKind,
matchLabelsToLabelSelectorValue,
} from '../../utils';
+import { useRedeployApplicationMutation } from '../../queries/useRedeployApplicationMutation';
type Props = {
environmentId: EnvironmentId;
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/RollbackApplicationButton.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/RollbackApplicationButton.tsx
index 078935390..e03ad6714 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/RollbackApplicationButton.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/RollbackApplicationButton.tsx
@@ -14,10 +14,6 @@ import { buildConfirmButton } from '@@/modals/utils';
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
import { Tooltip } from '@@/Tip/Tooltip';
-import {
- useApplicationRevisionList,
- usePatchApplicationMutation,
-} from '../../application.queries';
import {
applicationIsKind,
getRollbackPatchPayload,
@@ -25,6 +21,8 @@ import {
} from '../../utils';
import { Application } from '../../types';
import { appDeployMethodLabel } from '../../constants';
+import { useApplicationRevisionList } from '../../queries/useApplicationRevisionList';
+import { usePatchApplicationMutation } from '../../queries/usePatchApplicationMutation';
type Props = {
environmentId: EnvironmentId;
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationEventsDatatable.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationEventsDatatable.tsx
index f2046d55c..f578dc355 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationEventsDatatable.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationEventsDatatable.tsx
@@ -6,14 +6,12 @@ import { EnvironmentId } from '@/react/portainer/environments/types';
import { useTableState } from '@@/datatables/useTableState';
-import {
- useApplication,
- useApplicationPods,
- useApplicationServices,
-} from '../application.queries';
import { EventsDatatable } from '../../components/EventsDatatable';
import { useEvents } from '../../queries/useEvents';
import { AppKind } from '../types';
+import { useApplication } from '../queries/useApplication';
+import { useApplicationServices } from '../queries/useApplicationServices';
+import { useApplicationPods } from '../queries/useApplicationPods';
const storageKey = 'k8sAppEventsDatatable';
const settingsStore = createStore(storageKey, { id: 'Date', desc: true });
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx
index e9f964cc9..bd78f69c1 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx
@@ -34,12 +34,10 @@ import {
getTotalPods,
isExternalApplication,
} from '../utils';
-import {
- useApplication,
- usePatchApplicationMutation,
-} from '../application.queries';
import { Application, ApplicationPatch } from '../types';
import { useNamespaceQuery } from '../../namespaces/queries/useNamespaceQuery';
+import { useApplication } from '../queries/useApplication';
+import { usePatchApplicationMutation } from '../queries/usePatchApplicationMutation';
export function ApplicationSummaryWidget() {
const stateAndParams = useCurrentStateAndParams();
diff --git a/app/react/kubernetes/applications/DetailsView/PlacementsDatatable/usePlacementTableData.tsx b/app/react/kubernetes/applications/DetailsView/PlacementsDatatable/usePlacementTableData.tsx
index c6c4fcc61..b68abbff9 100644
--- a/app/react/kubernetes/applications/DetailsView/PlacementsDatatable/usePlacementTableData.tsx
+++ b/app/react/kubernetes/applications/DetailsView/PlacementsDatatable/usePlacementTableData.tsx
@@ -14,8 +14,9 @@ import {
} from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState';
-import { useApplication, useApplicationPods } from '../../application.queries';
import { Affinity, Label, NodePlacementRowData } from '../types';
+import { useApplication } from '../../queries/useApplication';
+import { useApplicationPods } from '../../queries/useApplicationPods';
interface TableSettings extends BasicTableSettings, RefreshableTableSettings {}
diff --git a/app/react/kubernetes/applications/DetailsView/useApplicationEventsTableData.tsx b/app/react/kubernetes/applications/DetailsView/useApplicationEventsTableData.tsx
index 9ab709b5d..099fcade4 100644
--- a/app/react/kubernetes/applications/DetailsView/useApplicationEventsTableData.tsx
+++ b/app/react/kubernetes/applications/DetailsView/useApplicationEventsTableData.tsx
@@ -5,12 +5,10 @@ import { createStore } from '@/react/kubernetes/datatables/default-kube-datatabl
import { useTableState } from '@@/datatables/useTableState';
-import {
- useApplication,
- useApplicationPods,
- useApplicationServices,
-} from '../application.queries';
import { useEvents } from '../../queries/useEvents';
+import { useApplication } from '../queries/useApplication';
+import { useApplicationServices } from '../queries/useApplicationServices';
+import { useApplicationPods } from '../queries/useApplicationPods';
const storageKey = 'k8sAppEventsDatatable';
const settingsStore = createStore(storageKey, { id: 'Date', desc: true });
diff --git a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx
index 4286f0e34..799d21fcc 100644
--- a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx
+++ b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx
@@ -1,28 +1,29 @@
import { useMemo } from 'react';
import { BoxIcon } from 'lucide-react';
import { groupBy, partition } from 'lodash';
+import { useRouter } from '@uirouter/react';
-import { useKubeStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton';
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
-import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
import { useAuthorizations } from '@/react/hooks/useUser';
import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
import { KubernetesApplicationTypes } from '@/kubernetes/models/application/models/appConstants';
+import { useEnvironment } from '@/react/portainer/environments/queries';
import { TableSettingsMenu } from '@@/datatables';
-import { useRepeater } from '@@/datatables/useRepeater';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { AddButton } from '@@/buttons';
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
import { NamespaceFilter } from '../ApplicationsStacksDatatable/NamespaceFilter';
-import { Namespace } from '../ApplicationsStacksDatatable/types';
-import { useApplications } from '../../application.queries';
import { PodKubernetesInstanceLabel, PodManagedByLabel } from '../../constants';
+import { useApplications } from '../../queries/useApplications';
+import { ApplicationsTableSettings } from '../useKubeAppsTableStore';
+import { useDeleteApplicationsMutation } from '../../queries/useDeleteApplicationsMutation';
+import { getStacksFromApplications } from '../ApplicationsStacksDatatable/getStacksFromApplications';
import { Application, ApplicationRowData, ConfigKind } from './types';
import { useColumns } from './useColumns';
@@ -31,35 +32,30 @@ import { SubRow } from './SubRow';
import { HelmInsightsBox } from './HelmInsightsBox';
export function ApplicationsDatatable({
- onRefresh,
- onRemove,
- namespace = '',
- namespaces,
- onNamespaceChange,
- hideStacks,
+ tableState,
+ hideStacks = false,
}: {
- onRefresh: () => void;
- onRemove: (selectedItems: Application[]) => void;
- namespace?: string;
- namespaces: Array;
- onNamespaceChange(namespace: string): void;
- hideStacks: boolean;
+ hideStacks?: boolean;
+ tableState: ApplicationsTableSettings & {
+ setSearch: (value: string) => void;
+ search: string;
+ };
}) {
- const envId = useEnvironmentId();
- const envQuery = useCurrentEnvironment();
- const namespaceListQuery = useNamespacesQuery(envId);
-
- const tableState = useKubeStore('kubernetes.applications', 'Name');
- useRepeater(tableState.autoRefreshRate, onRefresh);
-
- const hasWriteAuthQuery = useAuthorizations(
+ const router = useRouter();
+ const environmentId = useEnvironmentId();
+ const restrictSecretsQuery = useEnvironment(
+ environmentId,
+ (env) => env.Kubernetes.Configuration.RestrictSecrets
+ );
+ const namespaceListQuery = useNamespacesQuery(environmentId);
+ const { authorized: hasWriteAuth } = useAuthorizations(
'K8sApplicationsW',
undefined,
false
);
- const applicationsQuery = useApplications(envId, {
+ const applicationsQuery = useApplications(environmentId, {
refetchInterval: tableState.autoRefreshRate * 1000,
- namespace,
+ namespace: tableState.namespace,
withDependencies: true,
});
const applications = useApplicationsRowData(applicationsQuery.data);
@@ -69,20 +65,25 @@ export function ApplicationsDatatable({
(application) =>
!isSystemNamespace(application.ResourcePool, namespaceListQuery.data)
);
+ const stacks = getStacksFromApplications(filteredApplications);
+ const removeApplicationsMutation = useDeleteApplicationsMutation({
+ environmentId,
+ stacks,
+ reportStacks: false,
+ });
const columns = useColumns(hideStacks);
return (
!isSystemNamespace(row.original.ResourcePool, namespaceListQuery.data)
}
@@ -91,25 +92,22 @@ export function ApplicationsDatatable({
)}
renderTableActions={(selectedItems) =>
- hasWriteAuthQuery.authorized && (
+ hasWriteAuth && (
<>
onRemove(selectedItems)}
+ onConfirmed={() => handleRemoveApplications(selectedItems)}
/>
-
Add with form
-
>
)
@@ -123,18 +121,16 @@ export function ApplicationsDatatable({
-
-
@@ -143,6 +139,14 @@ export function ApplicationsDatatable({
}
/>
);
+
+ function handleRemoveApplications(applications: ApplicationRowData[]) {
+ removeApplicationsMutation.mutate(applications, {
+ onSuccess: () => {
+ router.stateService.reload();
+ },
+ });
+ }
}
function useApplicationsRowData(
diff --git a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/index.ts b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/index.ts
new file mode 100644
index 000000000..0864b2a59
--- /dev/null
+++ b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/index.ts
@@ -0,0 +1 @@
+export { ApplicationsDatatable } from './ApplicationsDatatable';
diff --git a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/ApplicationsStacksDatatable.tsx b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/ApplicationsStacksDatatable.tsx
index 123e2c54d..023c35aab 100644
--- a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/ApplicationsStacksDatatable.tsx
+++ b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/ApplicationsStacksDatatable.tsx
@@ -1,52 +1,44 @@
import { List } from 'lucide-react';
+import { useRouter } from '@uirouter/react';
-import { useAuthorizations } from '@/react/hooks/useUser';
+import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
-import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
-import { useTableState } from '@@/datatables/useTableState';
import { TableSettingsMenu } from '@@/datatables';
+import { DeleteButton } from '@@/buttons/DeleteButton';
-import { useApplications } from '../../application.queries';
+import { useApplications } from '../../queries/useApplications';
+import { ApplicationsTableSettings } from '../useKubeAppsTableStore';
+import { useDeleteApplicationsMutation } from '../../queries/useDeleteApplicationsMutation';
import { columns } from './columns';
import { SubRows } from './SubRows';
-import { Namespace, Stack } from './types';
import { NamespaceFilter } from './NamespaceFilter';
-import { TableActions } from './TableActions';
import { getStacksFromApplications } from './getStacksFromApplications';
-
-const storageKey = 'kubernetes.applications.stacks';
-
-const settingsStore = createStore(storageKey);
-
-interface Props {
- onRemove(selectedItems: Array
): void;
- namespace?: string;
- namespaces: Array;
- onNamespaceChange(namespace: string): void;
-}
+import { Stack } from './types';
export function ApplicationsStacksDatatable({
- onRemove,
- namespace = '',
- namespaces,
- onNamespaceChange,
-}: Props) {
- const tableState = useTableState(settingsStore, storageKey);
-
- const envId = useEnvironmentId();
- const applicationsQuery = useApplications(envId, {
+ tableState,
+}: {
+ tableState: ApplicationsTableSettings & {
+ setSearch: (value: string) => void;
+ search: string;
+ };
+}) {
+ const router = useRouter();
+ const environmentId = useEnvironmentId();
+ const namespaceListQuery = useNamespacesQuery(environmentId);
+ const { authorized: hasWriteAuth } = useAuthorizations('K8sApplicationsW');
+ const applicationsQuery = useApplications(environmentId, {
refetchInterval: tableState.autoRefreshRate * 1000,
- namespace,
+ namespace: tableState.namespace,
withDependencies: true,
});
- const namespaceListQuery = useNamespacesQuery(envId);
const applications = applicationsQuery.data ?? [];
const filteredApplications = tableState.showSystemResources
? applications
@@ -54,10 +46,12 @@ export function ApplicationsStacksDatatable({
(item) =>
!isSystemNamespace(item.ResourcePool, namespaceListQuery.data ?? [])
);
-
- const { authorized } = useAuthorizations('K8sApplicationsW');
-
const stacks = getStacksFromApplications(filteredApplications);
+ const removeApplicationsMutation = useDeleteApplicationsMutation({
+ environmentId,
+ stacks,
+ reportStacks: true,
+ });
return (
(
)}
- noWidget
description={
@@ -92,7 +85,14 @@ export function ApplicationsStacksDatatable({
}
renderTableActions={(selectedItems) => (
-
+
+ handleRemoveStacks(selectedItems)}
+ data-cy="k8sApp-removeStackButton"
+ />
+
)}
renderTableSettings={() => (
@@ -103,4 +103,13 @@ export function ApplicationsStacksDatatable({
data-cy="applications-stacks-datatable"
/>
);
+
+ function handleRemoveStacks(selectedItems: Stack[]) {
+ const applications = selectedItems.flatMap((stack) => stack.Applications);
+ removeApplicationsMutation.mutate(applications, {
+ onSuccess: () => {
+ router.stateService.reload();
+ },
+ });
+ }
}
diff --git a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/NamespaceFilter.tsx b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/NamespaceFilter.tsx
index c6b2b6f7d..5195d6d07 100644
--- a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/NamespaceFilter.tsx
+++ b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/NamespaceFilter.tsx
@@ -1,13 +1,16 @@
import { Filter } from 'lucide-react';
import { useEffect } from 'react';
+import { PortainerNamespace } from '@/react/kubernetes/namespaces/types';
+
import { Icon } from '@@/Icon';
import { Select } from '@@/form-components/Input';
import { InputGroup } from '@@/form-components/InputGroup';
-import { Namespace } from './types';
-
-function transformNamespaces(namespaces: Namespace[], showSystem?: boolean) {
+function transformNamespaces(
+ namespaces: PortainerNamespace[],
+ showSystem?: boolean
+) {
const transformedNamespaces = namespaces.map(({ Name, IsSystem }) => ({
label: IsSystem ? `${Name} - system` : Name,
value: Name,
@@ -26,7 +29,7 @@ export function NamespaceFilter({
onChange,
showSystem,
}: {
- namespaces: Namespace[];
+ namespaces: PortainerNamespace[];
value: string;
onChange: (value: string) => void;
showSystem?: boolean;
diff --git a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/TableActions.tsx b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/TableActions.tsx
deleted file mode 100644
index 732960fb2..000000000
--- a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/TableActions.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Authorized } from '@/react/hooks/useUser';
-
-import { DeleteButton } from '@@/buttons/DeleteButton';
-
-import { Stack } from './types';
-
-export function TableActions({
- selectedItems,
- onRemove,
-}: {
- selectedItems: Array;
- onRemove: (selectedItems: Array) => void;
-}) {
- return (
-
- onRemove(selectedItems)}
- data-cy="k8sApp-removeStackButton"
- />
-
- );
-}
diff --git a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/types.ts b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/types.ts
index 915c535fc..5f6f4736b 100644
--- a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/types.ts
+++ b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/types.ts
@@ -12,13 +12,6 @@ export interface TableSettings
RefreshableTableSettings,
SystemResourcesTableSettings {}
-export interface Namespace {
- Id: string;
- Name: string;
- Yaml: string;
- IsSystem?: boolean;
-}
-
export type Stack = {
Name: string;
ResourcePool: string;
diff --git a/app/react/kubernetes/applications/ListView/ApplicationsView.tsx b/app/react/kubernetes/applications/ListView/ApplicationsView.tsx
new file mode 100644
index 000000000..b69e67edb
--- /dev/null
+++ b/app/react/kubernetes/applications/ListView/ApplicationsView.tsx
@@ -0,0 +1,54 @@
+import { BoxIcon, List } from 'lucide-react';
+import { useCurrentStateAndParams } from '@uirouter/react';
+
+import { usePublicSettings } from '@/react/portainer/settings/queries/usePublicSettings';
+
+import { PageHeader } from '@@/PageHeader';
+import { Tab, WidgetTabs, findSelectedTabIndex } from '@@/Widget/WidgetTabs';
+
+import { ApplicationsDatatable } from './ApplicationsDatatable';
+import { ApplicationsStacksDatatable } from './ApplicationsStacksDatatable';
+import { useKubeAppsTableStore } from './useKubeAppsTableStore';
+
+export function ApplicationsView() {
+ const tableState = useKubeAppsTableStore('kubernetes.applications', 'Name');
+ const hideStacksQuery = usePublicSettings({
+ select: (settings) =>
+ settings.GlobalDeploymentOptions.hideStacksFunctionality,
+ });
+ const hideStacks = hideStacksQuery.isLoading || !!hideStacksQuery.data;
+
+ const tabs: Tab[] = [
+ {
+ name: 'Applications',
+ icon: BoxIcon,
+ widget: ,
+ selectedTabParam: 'applications',
+ },
+ {
+ name: 'Stacks',
+ icon: List,
+ widget: ,
+ selectedTabParam: 'stacks',
+ },
+ ];
+
+ const currentTabIndex = findSelectedTabIndex(
+ useCurrentStateAndParams(),
+ tabs
+ );
+
+ return (
+ <>
+
+ {hideStacks ? (
+
+ ) : (
+ <>
+
+ {tabs[currentTabIndex].widget}
+ >
+ )}
+ >
+ );
+}
diff --git a/app/react/kubernetes/applications/ListView/useKubeAppsTableStore.ts b/app/react/kubernetes/applications/ListView/useKubeAppsTableStore.ts
new file mode 100644
index 000000000..92c0eb7d6
--- /dev/null
+++ b/app/react/kubernetes/applications/ListView/useKubeAppsTableStore.ts
@@ -0,0 +1,55 @@
+import { useState } from 'react';
+
+import { TableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
+
+import {
+ refreshableSettings,
+ createPersistedStore,
+ ZustandSetFunc,
+} from '@@/datatables/types';
+import { useTableState } from '@@/datatables/useTableState';
+
+import { systemResourcesSettings } from '../../datatables/SystemResourcesSettings';
+
+interface NamespaceSelectSettings {
+ namespace: string;
+ setNamespace: (namespace: string) => void;
+}
+export type ApplicationsTableSettings = NamespaceSelectSettings & TableSettings;
+
+export function namespaceSelectSettings(
+ set: ZustandSetFunc,
+ namespace = 'default'
+): NamespaceSelectSettings {
+ return {
+ namespace,
+ setNamespace: (namespace: string) => set((s) => ({ ...s, namespace })),
+ };
+}
+
+export function createStore(
+ storageKey: string,
+ initialSortBy?: string | { id: string; desc: boolean },
+ create: (
+ set: ZustandSetFunc
+ ) => Omit = () => ({}) as T
+) {
+ return createPersistedStore(
+ storageKey,
+ initialSortBy,
+ (set) =>
+ ({
+ ...refreshableSettings(set),
+ ...systemResourcesSettings(set),
+ ...namespaceSelectSettings(set),
+ ...create(set),
+ }) as T
+ );
+}
+
+export function useKubeAppsTableStore(
+ ...args: Parameters>
+) {
+ const [store] = useState(() => createStore(...args));
+ return useTableState(store, args[0]);
+}
diff --git a/app/react/kubernetes/applications/application.queries.ts b/app/react/kubernetes/applications/application.queries.ts
deleted file mode 100644
index 8c38a6928..000000000
--- a/app/react/kubernetes/applications/application.queries.ts
+++ /dev/null
@@ -1,451 +0,0 @@
-import { UseQueryResult, useMutation, useQuery } from '@tanstack/react-query';
-import { Pod, PodList } from 'kubernetes-types/core/v1';
-
-import {
- queryClient,
- withError,
- withGlobalError,
-} from '@/react-tools/react-query';
-import { EnvironmentId } from '@/react/portainer/environments/types';
-import axios, { parseAxiosError } from '@/portainer/services/axios';
-
-import { getNamespaceServices } from '../services/service';
-import { parseKubernetesAxiosError } from '../axiosError';
-
-import {
- getApplication,
- patchApplication,
- getApplicationRevisionList,
-} from './application.service';
-import type { AppKind, Application, ApplicationPatch } from './types';
-import { Application as K8sApplication } from './ListView/ApplicationsDatatable/types';
-import { deletePod } from './pod.service';
-import { getNamespaceHorizontalPodAutoscalers } from './autoscaling.service';
-import { applicationIsKind, matchLabelsToLabelSelectorValue } from './utils';
-
-const queryKeys = {
- applications: (environmentId: EnvironmentId, params?: GetAppsParams) =>
- [
- 'environments',
- environmentId,
- 'kubernetes',
- 'applications',
- params,
- ] as const,
- application: (
- environmentId: EnvironmentId,
- namespace: string,
- name: string,
- yaml?: boolean
- ) =>
- [
- 'environments',
- environmentId,
- 'kubernetes',
- 'applications',
- namespace,
- name,
- yaml,
- ] as const,
- applicationRevisions: (
- environmentId: EnvironmentId,
- namespace: string,
- name: string,
- labelSelector?: string
- ) =>
- [
- 'environments',
- environmentId,
- 'kubernetes',
- 'applications',
- namespace,
- name,
- 'revisions',
- labelSelector,
- ] as const,
- applicationServices: (
- environmentId: EnvironmentId,
- namespace: string,
- name: string
- ) =>
- [
- 'environments',
- environmentId,
- 'kubernetes',
- 'applications',
- namespace,
- name,
- 'services',
- ] as const,
- ingressesForApplication: (
- environmentId: EnvironmentId,
- namespace: string,
- name: string
- ) =>
- [
- 'environments',
- environmentId,
- 'kubernetes',
- 'applications',
- namespace,
- name,
- 'ingresses',
- ] as const,
- applicationHorizontalPodAutoscalers: (
- environmentId: EnvironmentId,
- namespace: string,
- name: string
- ) =>
- [
- 'environments',
- environmentId,
- 'kubernetes',
- 'applications',
- namespace,
- name,
- 'horizontalpodautoscalers',
- ] as const,
- applicationPods: (
- environmentId: EnvironmentId,
- namespace: string,
- name: string
- ) =>
- [
- 'environments',
- environmentId,
- 'kubernetes',
- 'applications',
- namespace,
- name,
- 'pods',
- ] as const,
-};
-
-// when yaml is set to true, the expected return type is a string
-export function useApplication(
- environmentId: EnvironmentId,
- namespace: string,
- name: string,
- appKind?: AppKind,
- options?: { autoRefreshRate?: number; yaml?: boolean }
-): UseQueryResult {
- return useQuery(
- queryKeys.application(environmentId, namespace, name, options?.yaml),
- () =>
- getApplication(environmentId, namespace, name, appKind, options?.yaml),
- {
- ...withError('Unable to retrieve application'),
- refetchInterval() {
- return options?.autoRefreshRate ?? false;
- },
- }
- );
-}
-
-// test if I can get the previous revision
-// useQuery to get an application's previous revision by environmentId, namespace, appKind and labelSelector
-export function useApplicationRevisionList(
- environmentId: EnvironmentId,
- namespace: string,
- name: string,
- deploymentUid?: string,
- labelSelector?: string,
- appKind?: AppKind
-) {
- return useQuery(
- queryKeys.applicationRevisions(
- environmentId,
- namespace,
- name,
- labelSelector
- ),
- () =>
- getApplicationRevisionList(
- environmentId,
- namespace,
- deploymentUid,
- appKind,
- labelSelector
- ),
- {
- ...withError('Unable to retrieve application revisions'),
- enabled: !!labelSelector && !!appKind && !!deploymentUid,
- }
- );
-}
-
-// useApplicationServices returns a query for services that are related to the application (this doesn't include ingresses)
-// Filtering the services by the application selector labels is done in the front end because:
-// - The label selector query param in the kubernetes API filters by metadata.labels, but we need to filter the services by spec.selector
-// - The field selector query param in the kubernetes API can filter the services by spec.selector, but it doesn't support chaining with 'OR',
-// so we can't filter by services with at least one matching label. See: https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/#chained-selectors
-export function useApplicationServices(
- environmentId: EnvironmentId,
- namespace: string,
- appName: string,
- app?: Application
-) {
- return useQuery(
- queryKeys.applicationServices(environmentId, namespace, appName),
- async () => {
- if (!app) {
- return [];
- }
-
- // get the selector labels for the application
- const appSelectorLabels = applicationIsKind('Pod', app)
- ? app.metadata?.labels
- : app.spec?.template?.metadata?.labels;
-
- // get all services in the namespace and filter them by the application selector labels
- const services = await getNamespaceServices(environmentId, namespace);
- const filteredServices = services.filter((service) => {
- if (service.spec?.selector && appSelectorLabels) {
- const serviceSelectorLabels = service.spec.selector;
- // include the service if the service selector label matches at least one application selector label
- return Object.keys(appSelectorLabels).some(
- (key) =>
- serviceSelectorLabels[key] &&
- serviceSelectorLabels[key] === appSelectorLabels[key]
- );
- }
- return false;
- });
- return filteredServices;
- },
- { ...withError(`Unable to get services for ${appName}`), enabled: !!app }
- );
-}
-
-// useApplicationHorizontalPodAutoscalers returns a query for horizontal pod autoscalers that are related to the application
-export function useApplicationHorizontalPodAutoscaler(
- environmentId: EnvironmentId,
- namespace: string,
- appName: string,
- app?: Application
-) {
- return useQuery(
- queryKeys.applicationHorizontalPodAutoscalers(
- environmentId,
- namespace,
- appName
- ),
- async () => {
- if (!app) {
- return null;
- }
-
- const horizontalPodAutoscalers =
- await getNamespaceHorizontalPodAutoscalers(environmentId, namespace);
- const matchingHorizontalPodAutoscaler =
- horizontalPodAutoscalers.find((horizontalPodAutoscaler) => {
- const scaleTargetRef = horizontalPodAutoscaler.spec?.scaleTargetRef;
- if (scaleTargetRef) {
- const scaleTargetRefName = scaleTargetRef.name;
- const scaleTargetRefKind = scaleTargetRef.kind;
- // include the horizontal pod autoscaler if the scale target ref name and kind match the application name and kind
- return (
- scaleTargetRefName === app.metadata?.name &&
- scaleTargetRefKind === app.kind
- );
- }
- return false;
- }) || null;
- return matchingHorizontalPodAutoscaler;
- },
- {
- ...withError(
- `Unable to get horizontal pod autoscaler${
- app ? ` for ${app.metadata?.name}` : ''
- }`
- ),
- enabled: !!app,
- }
- );
-}
-
-// useApplicationPods returns a query for pods that are related to the application by the application selector labels
-export function useApplicationPods(
- environmentId: EnvironmentId,
- namespace: string,
- appName: string,
- app?: Application,
- options?: { autoRefreshRate?: number }
-) {
- return useQuery(
- queryKeys.applicationPods(environmentId, namespace, appName),
- async () => {
- if (applicationIsKind('Pod', app)) {
- return [app];
- }
- const appSelector = app?.spec?.selector;
- const labelSelector = matchLabelsToLabelSelectorValue(
- appSelector?.matchLabels
- );
-
- // get all pods in the namespace using the application selector as the label selector query param
- const pods = await getNamespacePods(
- environmentId,
- namespace,
- labelSelector
- );
- return pods;
- },
- {
- ...withError(`Unable to get pods for ${appName}`),
- enabled: !!app,
- refetchInterval() {
- return options?.autoRefreshRate ?? false;
- },
- }
- );
-}
-
-async function getNamespacePods(
- environmentId: EnvironmentId,
- namespace: string,
- labelSelector?: string
-) {
- try {
- const { data } = await axios.get(
- `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods`,
- {
- params: {
- labelSelector,
- },
- }
- );
- const items = (data.items || []).map(
- (pod) =>
- {
- ...pod,
- kind: 'Pod',
- apiVersion: data.apiVersion,
- }
- );
- return items;
- } catch (e) {
- throw parseKubernetesAxiosError(
- e,
- `Unable to retrieve Pods in namespace '${namespace}'`
- );
- }
-}
-
-// useQuery to patch an application by environmentId, namespace, name and patch payload
-export function usePatchApplicationMutation(
- environmentId: EnvironmentId,
- namespace: string,
- name: string
-) {
- return useMutation(
- ({
- appKind,
- patch,
- contentType = 'application/json-patch+json',
- }: {
- appKind: AppKind;
- patch: ApplicationPatch;
- contentType?:
- | 'application/json-patch+json'
- | 'application/strategic-merge-patch+json';
- }) =>
- patchApplication(
- environmentId,
- namespace,
- appKind,
- name,
- patch,
- contentType
- ),
- {
- onSuccess: () => {
- queryClient.invalidateQueries(
- queryKeys.application(environmentId, namespace, name)
- );
- },
- // patch application is used for patching and rollbacks, so handle the error where it's used instead of here
- }
- );
-}
-
-// useRedeployApplicationMutation gets all the pods for an application (using the matchLabels field in the labelSelector query param) and then deletes all of them, so that they are recreated
-export function useRedeployApplicationMutation(
- environmentId: number,
- namespace: string,
- name: string
-) {
- return useMutation(
- async ({ labelSelector }: { labelSelector: string }) => {
- try {
- // get only the pods that match the labelSelector for the application
- const pods = await getNamespacePods(
- environmentId,
- namespace,
- labelSelector
- );
- // delete all the pods to redeploy the application
- await Promise.all(
- pods.map((pod) => {
- if (pod?.metadata?.name) {
- return deletePod(environmentId, namespace, pod.metadata.name);
- }
- return Promise.resolve();
- })
- );
- } catch (error) {
- throw new Error(`Unable to redeploy application: ${error}`);
- }
- },
- {
- onSuccess: () => {
- queryClient.invalidateQueries(
- queryKeys.application(environmentId, namespace, name)
- );
- },
- ...withError('Unable to redeploy application'),
- }
- );
-}
-
-type GetAppsParams = {
- namespace?: string;
- nodeName?: string;
- withDependencies?: boolean;
-};
-
-type GetAppsQueryOptions = {
- refetchInterval?: number;
-} & GetAppsParams;
-
-// useQuery to get a list of all applications from an array of namespaces
-export function useApplications(
- environmentId: EnvironmentId,
- queryOptions?: GetAppsQueryOptions
-) {
- const { refetchInterval, ...params } = queryOptions ?? {};
- return useQuery(
- queryKeys.applications(environmentId, params),
- () => getApplications(environmentId, params),
- {
- refetchInterval,
- ...withGlobalError('Unable to retrieve applications'),
- }
- );
-}
-
-// get all applications from a namespace
-export async function getApplications(
- environmentId: EnvironmentId,
- params?: GetAppsParams
-) {
- try {
- const { data } = await axios.get(
- `/kubernetes/${environmentId}/applications`,
- { params }
- );
- return data;
- } catch (e) {
- throw parseAxiosError(e, 'Unable to retrieve applications');
- }
-}
diff --git a/app/react/kubernetes/applications/application.service.ts b/app/react/kubernetes/applications/application.service.ts
deleted file mode 100644
index ea6fed15b..000000000
--- a/app/react/kubernetes/applications/application.service.ts
+++ /dev/null
@@ -1,312 +0,0 @@
-import {
- Deployment,
- DaemonSet,
- StatefulSet,
- ReplicaSetList,
- ControllerRevisionList,
-} from 'kubernetes-types/apps/v1';
-
-import axios, { parseAxiosError } from '@/portainer/services/axios';
-import { EnvironmentId } from '@/react/portainer/environments/types';
-import { isFulfilled } from '@/portainer/helpers/promise-utils';
-
-import { parseKubernetesAxiosError } from '../axiosError';
-
-import { getPod, patchPod } from './pod.service';
-import { filterRevisionsByOwnerUid } from './utils';
-import { AppKind, Application, ApplicationPatch } from './types';
-import { appRevisionAnnotation } from './constants';
-
-// if not known, get the type of an application (Deployment, DaemonSet, StatefulSet or naked pod) by name
-export async function getApplication<
- T extends Application | string = Application,
->(
- environmentId: EnvironmentId,
- namespace: string,
- name: string,
- appKind?: AppKind,
- yaml?: boolean
-) {
- // if resourceType is known, get the application by type and name
- if (appKind) {
- switch (appKind) {
- case 'Deployment':
- case 'DaemonSet':
- case 'StatefulSet':
- return getApplicationByKind(
- environmentId,
- namespace,
- appKind,
- name,
- yaml
- );
- case 'Pod':
- return getPod(environmentId, namespace, name, yaml);
- default:
- throw new Error('Unknown resource type');
- }
- }
-
- // if resourceType is not known, get the application by name and return the first one that is fulfilled
- const [deployment, daemonSet, statefulSet, pod] = await Promise.allSettled([
- getApplicationByKind(
- environmentId,
- namespace,
- 'Deployment',
- name,
- yaml
- ),
- getApplicationByKind(
- environmentId,
- namespace,
- 'DaemonSet',
- name,
- yaml
- ),
- getApplicationByKind(
- environmentId,
- namespace,
- 'StatefulSet',
- name,
- yaml
- ),
- getPod(environmentId, namespace, name, yaml),
- ]);
-
- if (isFulfilled(deployment)) {
- return deployment.value;
- }
- if (isFulfilled(daemonSet)) {
- return daemonSet.value;
- }
- if (isFulfilled(statefulSet)) {
- return statefulSet.value;
- }
- if (isFulfilled(pod)) {
- return pod.value;
- }
- throw new Error('Unable to retrieve application');
-}
-
-export async function patchApplication(
- environmentId: EnvironmentId,
- namespace: string,
- appKind: AppKind,
- name: string,
- patch: ApplicationPatch,
- contentType: string = 'application/json-patch+json'
-) {
- switch (appKind) {
- case 'Deployment':
- return patchApplicationByKind(
- environmentId,
- namespace,
- appKind,
- name,
- patch,
- contentType
- );
- case 'DaemonSet':
- return patchApplicationByKind(
- environmentId,
- namespace,
- appKind,
- name,
- patch,
- contentType
- );
- case 'StatefulSet':
- return patchApplicationByKind(
- environmentId,
- namespace,
- appKind,
- name,
- patch,
- contentType
- );
- case 'Pod':
- return patchPod(environmentId, namespace, name, patch);
- default:
- throw new Error(`Unknown application kind ${appKind}`);
- }
-}
-
-async function patchApplicationByKind(
- environmentId: EnvironmentId,
- namespace: string,
- appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
- name: string,
- patch: ApplicationPatch,
- contentType = 'application/json-patch+json'
-) {
- try {
- const res = await axios.patch(
- buildUrl(environmentId, namespace, `${appKind}s`, name),
- patch,
- {
- headers: {
- 'Content-Type': contentType,
- },
- }
- );
- return res;
- } catch (e) {
- throw parseKubernetesAxiosError(e, 'Unable to patch application');
- }
-}
-
-async function getApplicationByKind<
- T extends Application | string = Application,
->(
- environmentId: EnvironmentId,
- namespace: string,
- appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
- name: string,
- yaml?: boolean
-) {
- try {
- const { data } = await axios.get(
- buildUrl(environmentId, namespace, `${appKind}s`, name),
- {
- headers: { Accept: yaml ? 'application/yaml' : 'application/json' },
- // this logic is to get the latest YAML response
- // axios-cache-adapter looks for the response headers to determine if the response should be cached
- // to avoid writing requestInterceptor, adding a query param to the request url
- params: yaml
- ? {
- _: Date.now(),
- }
- : null,
- }
- );
- return data;
- } catch (e) {
- throw parseKubernetesAxiosError(e, 'Unable to retrieve application');
- }
-}
-
-export async function getApplicationRevisionList(
- environmentId: EnvironmentId,
- namespace: string,
- deploymentUid?: string,
- appKind?: AppKind,
- labelSelector?: string
-) {
- if (!deploymentUid) {
- throw new Error('deploymentUid is required');
- }
- try {
- switch (appKind) {
- case 'Deployment': {
- const replicaSetList = await getReplicaSetList(
- environmentId,
- namespace,
- labelSelector
- );
- const replicaSets = replicaSetList.items;
- // keep only replicaset(s) which are owned by the deployment with the given uid
- const replicaSetsWithOwnerId = filterRevisionsByOwnerUid(
- replicaSets,
- deploymentUid
- );
- // keep only replicaset(s) that have been a version of the Deployment
- const replicaSetsWithRevisionAnnotations =
- replicaSetsWithOwnerId.filter(
- (rs) => !!rs.metadata?.annotations?.[appRevisionAnnotation]
- );
-
- return {
- ...replicaSetList,
- items: replicaSetsWithRevisionAnnotations,
- } as ReplicaSetList;
- }
- case 'DaemonSet':
- case 'StatefulSet': {
- const controllerRevisionList = await getControllerRevisionList(
- environmentId,
- namespace,
- labelSelector
- );
- const controllerRevisions = controllerRevisionList.items;
- // ensure the controller reference(s) is owned by the deployment with the given uid
- const controllerRevisionsWithOwnerId = filterRevisionsByOwnerUid(
- controllerRevisions,
- deploymentUid
- );
-
- return {
- ...controllerRevisionList,
- items: controllerRevisionsWithOwnerId,
- } as ControllerRevisionList;
- }
- default:
- throw new Error(`Unknown application kind ${appKind}`);
- }
- } catch (e) {
- throw parseAxiosError(
- e as Error,
- `Unable to retrieve revisions for ${appKind}`
- );
- }
-}
-
-export async function getReplicaSetList(
- environmentId: EnvironmentId,
- namespace: string,
- labelSelector?: string
-) {
- try {
- const { data } = await axios.get(
- buildUrl(environmentId, namespace, 'ReplicaSets'),
- {
- params: {
- labelSelector,
- },
- }
- );
- return data;
- } catch (e) {
- throw parseKubernetesAxiosError(e, 'Unable to retrieve ReplicaSets');
- }
-}
-
-export async function getControllerRevisionList(
- environmentId: EnvironmentId,
- namespace: string,
- labelSelector?: string
-) {
- try {
- const { data } = await axios.get(
- buildUrl(environmentId, namespace, 'ControllerRevisions'),
- {
- params: {
- labelSelector,
- },
- }
- );
- return data;
- } catch (e) {
- throw parseKubernetesAxiosError(
- e,
- 'Unable to retrieve ControllerRevisions'
- );
- }
-}
-
-function buildUrl(
- environmentId: EnvironmentId,
- namespace: string,
- appKind:
- | 'Deployments'
- | 'DaemonSets'
- | 'StatefulSets'
- | 'ReplicaSets'
- | 'ControllerRevisions',
- name?: string
-) {
- let baseUrl = `/endpoints/${environmentId}/kubernetes/apis/apps/v1/namespaces/${namespace}/${appKind.toLowerCase()}`;
- if (name) {
- baseUrl += `/${name}`;
- }
- return baseUrl;
-}
diff --git a/app/react/kubernetes/applications/pod.service.ts b/app/react/kubernetes/applications/pod.service.ts
deleted file mode 100644
index f5f3ed683..000000000
--- a/app/react/kubernetes/applications/pod.service.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { Pod } from 'kubernetes-types/core/v1';
-
-import { EnvironmentId } from '@/react/portainer/environments/types';
-import axios, { parseAxiosError } from '@/portainer/services/axios';
-
-import { parseKubernetesAxiosError } from '../axiosError';
-
-import { ApplicationPatch } from './types';
-
-export async function getPod(
- environmentId: EnvironmentId,
- namespace: string,
- name: string,
- yaml?: boolean
-) {
- try {
- const { data } = await axios.get(
- buildUrl(environmentId, namespace, name),
- {
- headers: { Accept: yaml ? 'application/yaml' : 'application/json' },
- }
- );
- return data;
- } catch (e) {
- throw parseKubernetesAxiosError(e, 'Unable to retrieve pod');
- }
-}
-
-export async function patchPod(
- environmentId: EnvironmentId,
- namespace: string,
- name: string,
- patch: ApplicationPatch
-) {
- try {
- return await axios.patch(
- buildUrl(environmentId, namespace, name),
- patch,
- {
- headers: {
- 'Content-Type': 'application/json-patch+json',
- },
- }
- );
- } catch (e) {
- throw parseAxiosError(e, 'Unable to update pod');
- }
-}
-
-export async function deletePod(
- environmentId: EnvironmentId,
- namespace: string,
- name: string
-) {
- try {
- return await axios.delete(buildUrl(environmentId, namespace, name));
- } catch (e) {
- throw parseKubernetesAxiosError(e as Error, 'Unable to delete pod');
- }
-}
-
-export function buildUrl(
- environmentId: EnvironmentId,
- namespace: string,
- name?: string
-) {
- let baseUrl = `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods`;
- if (name) {
- baseUrl += `/${name}`;
- }
- return baseUrl;
-}
diff --git a/app/react/kubernetes/applications/queries/getPods.ts b/app/react/kubernetes/applications/queries/getPods.ts
new file mode 100644
index 000000000..494b4df2d
--- /dev/null
+++ b/app/react/kubernetes/applications/queries/getPods.ts
@@ -0,0 +1,37 @@
+import { Pod, PodList } from 'kubernetes-types/core/v1';
+
+import { EnvironmentId } from '@/react/portainer/environments/types';
+import axios from '@/portainer/services/axios';
+
+import { parseKubernetesAxiosError } from '../../axiosError';
+
+export async function getPods(
+ environmentId: EnvironmentId,
+ namespace: string,
+ labelSelector?: string
+) {
+ try {
+ const { data } = await axios.get(
+ `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods`,
+ {
+ params: {
+ labelSelector,
+ },
+ }
+ );
+ const items = (data.items || []).map(
+ (pod) =>
+ {
+ ...pod,
+ kind: 'Pod',
+ apiVersion: data.apiVersion,
+ }
+ );
+ return items;
+ } catch (e) {
+ throw parseKubernetesAxiosError(
+ e,
+ `Unable to retrieve Pods in namespace '${namespace}'`
+ );
+ }
+}
diff --git a/app/react/kubernetes/applications/queries/query-keys.ts b/app/react/kubernetes/applications/queries/query-keys.ts
new file mode 100644
index 000000000..87c042dc4
--- /dev/null
+++ b/app/react/kubernetes/applications/queries/query-keys.ts
@@ -0,0 +1,105 @@
+import { EnvironmentId } from '@/react/portainer/environments/types';
+
+export type GetAppsParams = {
+ namespace?: string;
+ nodeName?: string;
+ withDependencies?: boolean;
+};
+
+export const queryKeys = {
+ applications: (environmentId: EnvironmentId, params?: GetAppsParams) =>
+ [
+ 'environments',
+ environmentId,
+ 'kubernetes',
+ 'applications',
+ params,
+ ] as const,
+ application: (
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string,
+ yaml?: boolean
+ ) =>
+ [
+ 'environments',
+ environmentId,
+ 'kubernetes',
+ 'applications',
+ namespace,
+ name,
+ yaml,
+ ] as const,
+ applicationRevisions: (
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string,
+ labelSelector?: string
+ ) =>
+ [
+ 'environments',
+ environmentId,
+ 'kubernetes',
+ 'applications',
+ namespace,
+ name,
+ 'revisions',
+ labelSelector,
+ ] as const,
+ applicationServices: (
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string
+ ) =>
+ [
+ 'environments',
+ environmentId,
+ 'kubernetes',
+ 'applications',
+ namespace,
+ name,
+ 'services',
+ ] as const,
+ ingressesForApplication: (
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string
+ ) =>
+ [
+ 'environments',
+ environmentId,
+ 'kubernetes',
+ 'applications',
+ namespace,
+ name,
+ 'ingresses',
+ ] as const,
+ applicationHorizontalPodAutoscalers: (
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string
+ ) =>
+ [
+ 'environments',
+ environmentId,
+ 'kubernetes',
+ 'applications',
+ namespace,
+ name,
+ 'horizontalpodautoscalers',
+ ] as const,
+ applicationPods: (
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string
+ ) =>
+ [
+ 'environments',
+ environmentId,
+ 'kubernetes',
+ 'applications',
+ namespace,
+ name,
+ 'pods',
+ ] as const,
+};
diff --git a/app/react/kubernetes/applications/queries/useApplication.ts b/app/react/kubernetes/applications/queries/useApplication.ts
new file mode 100644
index 000000000..7632ac76c
--- /dev/null
+++ b/app/react/kubernetes/applications/queries/useApplication.ts
@@ -0,0 +1,168 @@
+import { UseQueryResult, useQuery } from '@tanstack/react-query';
+import { DaemonSet, Deployment, StatefulSet } from 'kubernetes-types/apps/v1';
+import { Pod } from 'kubernetes-types/core/v1';
+
+import { withGlobalError } from '@/react-tools/react-query';
+import { EnvironmentId } from '@/react/portainer/environments/types';
+import { isFulfilled } from '@/portainer/helpers/promise-utils';
+import axios from '@/portainer/services/axios';
+
+import { AppKind, Application } from '../types';
+import { parseKubernetesAxiosError } from '../../axiosError';
+
+import { queryKeys } from './query-keys';
+
+/**
+ * @returns an application from the Kubernetes API proxy (deployment, statefulset, daemonSet or pod)
+ * when yaml is set to true, the expected return type is a string
+ */
+export function useApplication(
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string,
+ appKind?: AppKind,
+ options?: { autoRefreshRate?: number; yaml?: boolean }
+): UseQueryResult {
+ return useQuery(
+ queryKeys.application(environmentId, namespace, name, options?.yaml),
+ () =>
+ getApplication(environmentId, namespace, name, appKind, options?.yaml),
+ {
+ ...withGlobalError('Unable to retrieve application'),
+ refetchInterval() {
+ return options?.autoRefreshRate ?? false;
+ },
+ }
+ );
+}
+
+// if not known, get the type of an application (Deployment, DaemonSet, StatefulSet or naked pod) by name
+async function getApplication(
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string,
+ appKind?: AppKind,
+ yaml?: boolean
+) {
+ // if resourceType is known, get the application by type and name
+ if (appKind) {
+ switch (appKind) {
+ case 'Deployment':
+ case 'DaemonSet':
+ case 'StatefulSet':
+ return getApplicationByKind(
+ environmentId,
+ namespace,
+ appKind,
+ name,
+ yaml
+ );
+ case 'Pod':
+ return getPod(environmentId, namespace, name, yaml);
+ default:
+ throw new Error('Unknown resource type');
+ }
+ }
+
+ // if resourceType is not known, get the application by name and return the first one that is fulfilled
+ const [deployment, daemonSet, statefulSet, pod] = await Promise.allSettled([
+ getApplicationByKind(
+ environmentId,
+ namespace,
+ 'Deployment',
+ name,
+ yaml
+ ),
+ getApplicationByKind(
+ environmentId,
+ namespace,
+ 'DaemonSet',
+ name,
+ yaml
+ ),
+ getApplicationByKind(
+ environmentId,
+ namespace,
+ 'StatefulSet',
+ name,
+ yaml
+ ),
+ getPod(environmentId, namespace, name, yaml),
+ ]);
+
+ if (isFulfilled(deployment)) {
+ return deployment.value;
+ }
+ if (isFulfilled(daemonSet)) {
+ return daemonSet.value;
+ }
+ if (isFulfilled(statefulSet)) {
+ return statefulSet.value;
+ }
+ if (isFulfilled(pod)) {
+ return pod.value;
+ }
+ throw new Error('Unable to retrieve application');
+}
+
+async function getPod(
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string,
+ yaml?: boolean
+) {
+ try {
+ const { data } = await axios.get(
+ `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods/${name}`,
+ {
+ headers: { Accept: yaml ? 'application/yaml' : 'application/json' },
+ }
+ );
+ return data;
+ } catch (e) {
+ throw parseKubernetesAxiosError(e, 'Unable to retrieve pod');
+ }
+}
+
+async function getApplicationByKind<
+ T extends Application | string = Application,
+>(
+ environmentId: EnvironmentId,
+ namespace: string,
+ appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
+ name: string,
+ yaml?: boolean
+) {
+ try {
+ const { data } = await axios.get(
+ buildUrl(environmentId, namespace, `${appKind}s`, name),
+ {
+ headers: { Accept: yaml ? 'application/yaml' : 'application/json' },
+ // this logic is to get the latest YAML response
+ // axios-cache-adapter looks for the response headers to determine if the response should be cached
+ // to avoid writing requestInterceptor, adding a query param to the request url
+ params: yaml
+ ? {
+ _: Date.now(),
+ }
+ : null,
+ }
+ );
+ return data;
+ } catch (e) {
+ throw parseKubernetesAxiosError(e, 'Unable to retrieve application');
+ }
+}
+
+function buildUrl(
+ environmentId: EnvironmentId,
+ namespace: string,
+ appKind: 'Deployments' | 'DaemonSets' | 'StatefulSets',
+ name?: string
+) {
+ let baseUrl = `/endpoints/${environmentId}/kubernetes/apis/apps/v1/namespaces/${namespace}/${appKind.toLowerCase()}`;
+ if (name) {
+ baseUrl += `/${name}`;
+ }
+ return baseUrl;
+}
diff --git a/app/react/kubernetes/applications/queries/useApplicationHorizontalPodAutoscaler.ts b/app/react/kubernetes/applications/queries/useApplicationHorizontalPodAutoscaler.ts
new file mode 100644
index 000000000..ac3ad9799
--- /dev/null
+++ b/app/react/kubernetes/applications/queries/useApplicationHorizontalPodAutoscaler.ts
@@ -0,0 +1,76 @@
+import { useQuery } from '@tanstack/react-query';
+import { HorizontalPodAutoscalerList } from 'kubernetes-types/autoscaling/v1';
+
+import { withGlobalError } from '@/react-tools/react-query';
+import { EnvironmentId } from '@/react/portainer/environments/types';
+import axios from '@/portainer/services/axios';
+
+import type { Application } from '../types';
+import { parseKubernetesAxiosError } from '../../axiosError';
+
+import { queryKeys } from './query-keys';
+
+// useApplicationHorizontalPodAutoscalers returns a query for horizontal pod autoscalers that are related to the application
+export function useApplicationHorizontalPodAutoscaler(
+ environmentId: EnvironmentId,
+ namespace: string,
+ appName: string,
+ app?: Application
+) {
+ return useQuery(
+ queryKeys.applicationHorizontalPodAutoscalers(
+ environmentId,
+ namespace,
+ appName
+ ),
+ async () => {
+ if (!app) {
+ return null;
+ }
+
+ const horizontalPodAutoscalers =
+ await getNamespaceHorizontalPodAutoscalers(environmentId, namespace);
+ const matchingHorizontalPodAutoscaler =
+ horizontalPodAutoscalers.find((horizontalPodAutoscaler) => {
+ const scaleTargetRef = horizontalPodAutoscaler.spec?.scaleTargetRef;
+ if (scaleTargetRef) {
+ const scaleTargetRefName = scaleTargetRef.name;
+ const scaleTargetRefKind = scaleTargetRef.kind;
+ // include the horizontal pod autoscaler if the scale target ref name and kind match the application name and kind
+ return (
+ scaleTargetRefName === app.metadata?.name &&
+ scaleTargetRefKind === app.kind
+ );
+ }
+ return false;
+ }) || null;
+ return matchingHorizontalPodAutoscaler;
+ },
+ {
+ ...withGlobalError(
+ `Unable to get horizontal pod autoscaler${
+ app ? ` for ${app.metadata?.name}` : ''
+ }`
+ ),
+ enabled: !!app,
+ }
+ );
+}
+
+async function getNamespaceHorizontalPodAutoscalers(
+ environmentId: EnvironmentId,
+ namespace: string
+) {
+ try {
+ const { data: autoScalarList } =
+ await axios.get(
+ `/endpoints/${environmentId}/kubernetes/apis/autoscaling/v1/namespaces/${namespace}/horizontalpodautoscalers`
+ );
+ return autoScalarList.items;
+ } catch (e) {
+ throw parseKubernetesAxiosError(
+ e,
+ 'Unable to retrieve horizontal pod autoscalers'
+ );
+ }
+}
diff --git a/app/react/kubernetes/applications/queries/useApplicationPods.ts b/app/react/kubernetes/applications/queries/useApplicationPods.ts
new file mode 100644
index 000000000..03042ee73
--- /dev/null
+++ b/app/react/kubernetes/applications/queries/useApplicationPods.ts
@@ -0,0 +1,44 @@
+import { useQuery } from '@tanstack/react-query';
+import { Pod } from 'kubernetes-types/core/v1';
+
+import { withGlobalError } from '@/react-tools/react-query';
+import { EnvironmentId } from '@/react/portainer/environments/types';
+
+import { applicationIsKind, matchLabelsToLabelSelectorValue } from '../utils';
+import { Application } from '../types';
+
+import { queryKeys } from './query-keys';
+import { getPods } from './getPods';
+
+// useApplicationPods returns a query for pods that are related to the application by the application selector labels
+export function useApplicationPods(
+ environmentId: EnvironmentId,
+ namespace: string,
+ appName: string,
+ app?: Application,
+ options?: { autoRefreshRate?: number }
+) {
+ return useQuery(
+ queryKeys.applicationPods(environmentId, namespace, appName),
+ async () => {
+ if (applicationIsKind('Pod', app)) {
+ return [app];
+ }
+ const appSelector = app?.spec?.selector;
+ const labelSelector = matchLabelsToLabelSelectorValue(
+ appSelector?.matchLabels
+ );
+
+ // get all pods in the namespace using the application selector as the label selector query param
+ const pods = await getPods(environmentId, namespace, labelSelector);
+ return pods;
+ },
+ {
+ ...withGlobalError(`Unable to get pods for ${appName}`),
+ enabled: !!app,
+ refetchInterval() {
+ return options?.autoRefreshRate ?? false;
+ },
+ }
+ );
+}
diff --git a/app/react/kubernetes/applications/queries/useApplicationRevisionList.ts b/app/react/kubernetes/applications/queries/useApplicationRevisionList.ts
new file mode 100644
index 000000000..afef43301
--- /dev/null
+++ b/app/react/kubernetes/applications/queries/useApplicationRevisionList.ts
@@ -0,0 +1,173 @@
+import { useQuery } from '@tanstack/react-query';
+import {
+ ReplicaSetList,
+ ControllerRevisionList,
+} from 'kubernetes-types/apps/v1';
+
+import { withGlobalError } from '@/react-tools/react-query';
+import { EnvironmentId } from '@/react/portainer/environments/types';
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+
+import { AppKind } from '../types';
+import { appRevisionAnnotation } from '../constants';
+import { filterRevisionsByOwnerUid } from '../utils';
+import { parseKubernetesAxiosError } from '../../axiosError';
+
+import { queryKeys } from './query-keys';
+
+// useQuery to get an application's previous revision by environmentId, namespace, appKind and labelSelector
+export function useApplicationRevisionList(
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string,
+ deploymentUid?: string,
+ labelSelector?: string,
+ appKind?: AppKind
+) {
+ return useQuery(
+ queryKeys.applicationRevisions(
+ environmentId,
+ namespace,
+ name,
+ labelSelector
+ ),
+ () =>
+ getApplicationRevisionList(
+ environmentId,
+ namespace,
+ deploymentUid,
+ appKind,
+ labelSelector
+ ),
+ {
+ ...withGlobalError('Unable to retrieve application revisions'),
+ enabled: !!labelSelector && !!appKind && !!deploymentUid,
+ }
+ );
+}
+
+async function getApplicationRevisionList(
+ environmentId: EnvironmentId,
+ namespace: string,
+ deploymentUid?: string,
+ appKind?: AppKind,
+ labelSelector?: string
+) {
+ if (!deploymentUid) {
+ throw new Error('deploymentUid is required');
+ }
+ try {
+ switch (appKind) {
+ case 'Deployment': {
+ const replicaSetList = await getReplicaSetList(
+ environmentId,
+ namespace,
+ labelSelector
+ );
+ const replicaSets = replicaSetList.items;
+ // keep only replicaset(s) which are owned by the deployment with the given uid
+ const replicaSetsWithOwnerId = filterRevisionsByOwnerUid(
+ replicaSets,
+ deploymentUid
+ );
+ // keep only replicaset(s) that have been a version of the Deployment
+ const replicaSetsWithRevisionAnnotations =
+ replicaSetsWithOwnerId.filter(
+ (rs) => !!rs.metadata?.annotations?.[appRevisionAnnotation]
+ );
+
+ return {
+ ...replicaSetList,
+ items: replicaSetsWithRevisionAnnotations,
+ } as ReplicaSetList;
+ }
+ case 'DaemonSet':
+ case 'StatefulSet': {
+ const controllerRevisionList = await getControllerRevisionList(
+ environmentId,
+ namespace,
+ labelSelector
+ );
+ const controllerRevisions = controllerRevisionList.items;
+ // ensure the controller reference(s) is owned by the deployment with the given uid
+ const controllerRevisionsWithOwnerId = filterRevisionsByOwnerUid(
+ controllerRevisions,
+ deploymentUid
+ );
+
+ return {
+ ...controllerRevisionList,
+ items: controllerRevisionsWithOwnerId,
+ } as ControllerRevisionList;
+ }
+ default:
+ throw new Error(`Unknown application kind ${appKind}`);
+ }
+ } catch (e) {
+ throw parseAxiosError(
+ e as Error,
+ `Unable to retrieve revisions for ${appKind}`
+ );
+ }
+}
+
+async function getReplicaSetList(
+ environmentId: EnvironmentId,
+ namespace: string,
+ labelSelector?: string
+) {
+ try {
+ const { data } = await axios.get(
+ buildUrl(environmentId, namespace, 'ReplicaSets'),
+ {
+ params: {
+ labelSelector,
+ },
+ }
+ );
+ return data;
+ } catch (e) {
+ throw parseKubernetesAxiosError(e, 'Unable to retrieve ReplicaSets');
+ }
+}
+
+async function getControllerRevisionList(
+ environmentId: EnvironmentId,
+ namespace: string,
+ labelSelector?: string
+) {
+ try {
+ const { data } = await axios.get(
+ buildUrl(environmentId, namespace, 'ControllerRevisions'),
+ {
+ params: {
+ labelSelector,
+ },
+ }
+ );
+ return data;
+ } catch (e) {
+ throw parseKubernetesAxiosError(
+ e,
+ 'Unable to retrieve ControllerRevisions'
+ );
+ }
+}
+
+function buildUrl(
+ environmentId: EnvironmentId,
+ namespace: string,
+ appKind:
+ | 'Deployments'
+ | 'DaemonSets'
+ | 'StatefulSets'
+ | 'ReplicaSets'
+ | 'ControllerRevisions',
+ name?: string
+) {
+ let baseUrl = `/endpoints/${environmentId}/kubernetes/apis/apps/v1/namespaces/${namespace}/${appKind.toLowerCase()}`;
+ if (name) {
+ baseUrl += `/${name}`;
+ }
+ return baseUrl;
+}
diff --git a/app/react/kubernetes/applications/queries/useApplicationServices.ts b/app/react/kubernetes/applications/queries/useApplicationServices.ts
new file mode 100644
index 000000000..5cfc0bc17
--- /dev/null
+++ b/app/react/kubernetes/applications/queries/useApplicationServices.ts
@@ -0,0 +1,57 @@
+import { useQuery } from '@tanstack/react-query';
+import { Pod } from 'kubernetes-types/core/v1';
+
+import { withGlobalError } from '@/react-tools/react-query';
+import { EnvironmentId } from '@/react/portainer/environments/types';
+
+import { getNamespaceServices } from '../../services/service';
+import type { Application } from '../types';
+import { applicationIsKind } from '../utils';
+
+import { queryKeys } from './query-keys';
+
+// useApplicationServices returns a query for services that are related to the application (this doesn't include ingresses)
+// Filtering the services by the application selector labels is done in the front end because:
+// - The label selector query param in the kubernetes API filters by metadata.labels, but we need to filter the services by spec.selector
+// - The field selector query param in the kubernetes API can filter the services by spec.selector, but it doesn't support chaining with 'OR',
+// so we can't filter by services with at least one matching label. See: https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/#chained-selectors
+export function useApplicationServices(
+ environmentId: EnvironmentId,
+ namespace: string,
+ appName: string,
+ app?: Application
+) {
+ return useQuery(
+ queryKeys.applicationServices(environmentId, namespace, appName),
+ async () => {
+ if (!app) {
+ return [];
+ }
+
+ // get the selector labels for the application
+ const appSelectorLabels = applicationIsKind('Pod', app)
+ ? app.metadata?.labels
+ : app.spec?.template?.metadata?.labels;
+
+ // get all services in the namespace and filter them by the application selector labels
+ const services = await getNamespaceServices(environmentId, namespace);
+ const filteredServices = services.filter((service) => {
+ if (service.spec?.selector && appSelectorLabels) {
+ const serviceSelectorLabels = service.spec.selector;
+ // include the service if the service selector label matches at least one application selector label
+ return Object.keys(appSelectorLabels).some(
+ (key) =>
+ serviceSelectorLabels[key] &&
+ serviceSelectorLabels[key] === appSelectorLabels[key]
+ );
+ }
+ return false;
+ });
+ return filteredServices;
+ },
+ {
+ ...withGlobalError(`Unable to get services for ${appName}`),
+ enabled: !!app,
+ }
+ );
+}
diff --git a/app/react/kubernetes/applications/queries/useApplications.ts b/app/react/kubernetes/applications/queries/useApplications.ts
new file mode 100644
index 000000000..9d09ffad0
--- /dev/null
+++ b/app/react/kubernetes/applications/queries/useApplications.ts
@@ -0,0 +1,54 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { withGlobalError } from '@/react-tools/react-query';
+import { EnvironmentId } from '@/react/portainer/environments/types';
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+
+import { Application } from '../ListView/ApplicationsDatatable/types';
+
+import { queryKeys } from './query-keys';
+
+type GetAppsParams = {
+ namespace?: string;
+ nodeName?: string;
+ withDependencies?: boolean;
+};
+
+type GetAppsQueryOptions = {
+ refetchInterval?: number;
+} & GetAppsParams;
+
+/**
+ * @returns a UseQueryResult that can be used to fetch a list of applications from an array of namespaces.
+ * This api call goes to the portainer api, not the kubernetes proxy api.
+ */
+export function useApplications(
+ environmentId: EnvironmentId,
+ queryOptions?: GetAppsQueryOptions
+) {
+ const { refetchInterval, ...params } = queryOptions ?? {};
+ return useQuery(
+ queryKeys.applications(environmentId, params),
+ () => getApplications(environmentId, params),
+ {
+ refetchInterval,
+ ...withGlobalError('Unable to retrieve applications'),
+ }
+ );
+}
+
+// get all applications from a namespace
+export async function getApplications(
+ environmentId: EnvironmentId,
+ params?: GetAppsParams
+) {
+ try {
+ const { data } = await axios.get(
+ `/kubernetes/${environmentId}/applications`,
+ { params }
+ );
+ return data;
+ } catch (e) {
+ throw parseAxiosError(e, 'Unable to retrieve applications');
+ }
+}
diff --git a/app/react/kubernetes/applications/queries/useDeleteApplicationsMutation.ts b/app/react/kubernetes/applications/queries/useDeleteApplicationsMutation.ts
new file mode 100644
index 000000000..732f6dbb5
--- /dev/null
+++ b/app/react/kubernetes/applications/queries/useDeleteApplicationsMutation.ts
@@ -0,0 +1,191 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { EnvironmentId } from '@/react/portainer/environments/types';
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+import { getAllSettledItems } from '@/portainer/helpers/promise-utils';
+import { withGlobalError } from '@/react-tools/react-query';
+import { notifyError, notifySuccess } from '@/portainer/services/notifications';
+import { pluralize } from '@/portainer/helpers/strings';
+
+import { parseKubernetesAxiosError } from '../../axiosError';
+import { ApplicationRowData } from '../ListView/ApplicationsDatatable/types';
+import { Stack } from '../ListView/ApplicationsStacksDatatable/types';
+
+import { queryKeys } from './query-keys';
+
+export function useDeleteApplicationsMutation({
+ environmentId,
+ stacks,
+ reportStacks,
+}: {
+ environmentId: EnvironmentId;
+ stacks: Stack[];
+ reportStacks?: boolean;
+}) {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (applications: ApplicationRowData[]) =>
+ deleteApplications(applications, stacks, environmentId),
+ onSuccess: ({ settledAppDeletions, settledStackDeletions }) => {
+ // one error notification per rejected item
+ settledAppDeletions.rejectedItems.forEach(({ item, reason }) => {
+ notifyError(
+ `Failed to remove application '${item.Name}'`,
+ new Error(reason)
+ );
+ });
+ settledStackDeletions.rejectedItems.forEach(({ item, reason }) => {
+ notifyError(`Failed to remove stack '${item.Name}'`, new Error(reason));
+ });
+
+ // one success notification for all fulfilled items
+ if (settledAppDeletions.fulfilledItems.length && !reportStacks) {
+ notifySuccess(
+ `${pluralize(
+ settledAppDeletions.fulfilledItems.length,
+ 'Application'
+ )} successfully removed`,
+ settledAppDeletions.fulfilledItems.map((item) => item.Name).join(', ')
+ );
+ }
+ if (settledStackDeletions.fulfilledItems.length && reportStacks) {
+ notifySuccess(
+ `${pluralize(
+ settledStackDeletions.fulfilledItems.length,
+ 'Stack'
+ )} successfully removed`,
+ settledStackDeletions.fulfilledItems
+ .map((item) => item.Name)
+ .join(', ')
+ );
+ }
+ queryClient.invalidateQueries(queryKeys.applications(environmentId));
+ },
+ ...withGlobalError('Unable to remove applications'),
+ });
+}
+
+async function deleteApplications(
+ applications: ApplicationRowData[],
+ stacks: Stack[],
+ environmentId: EnvironmentId
+) {
+ const settledAppDeletions = await getAllSettledItems(
+ applications,
+ (application) => deleteApplication(application, stacks, environmentId)
+ );
+
+ // Delete stacks that have no applications left (stacks object has been mutated by deleteApplication)
+ const stacksToDelete = stacks.filter(
+ (stack) => stack.Applications.length === 0
+ );
+ const settledStackDeletions = await getAllSettledItems(
+ stacksToDelete,
+ (stack) => deleteStack(stack, environmentId)
+ );
+
+ return { settledAppDeletions, settledStackDeletions };
+}
+
+async function deleteStack(stack: Stack, environmentId: EnvironmentId) {
+ try {
+ await axios.delete(`/stacks/name/${stack.Name}`, {
+ params: {
+ external: false,
+ name: stack.Name,
+ endpointId: environmentId,
+ namespace: stack.ResourcePool,
+ },
+ });
+ } catch (error) {
+ throw parseAxiosError(error, 'Unable to remove stack');
+ }
+}
+
+async function deleteApplication(
+ application: ApplicationRowData,
+ stacks: Stack[],
+ environmentId: EnvironmentId
+) {
+ switch (application.ApplicationType) {
+ case 'Deployment':
+ case 'DaemonSet':
+ case 'StatefulSet':
+ await deleteKubernetesApplication(application, stacks, environmentId);
+ break;
+ case 'Pod':
+ await deletePodApplication(application, stacks, environmentId);
+ break;
+ case 'Helm':
+ await uninstallHelmApplication(application, environmentId);
+ break;
+ default:
+ throw new Error(
+ `Unknown application type: ${application.ApplicationType}`
+ );
+ }
+}
+
+async function deleteKubernetesApplication(
+ application: ApplicationRowData,
+ stacks: Stack[],
+ environmentId: EnvironmentId
+) {
+ try {
+ await axios.delete(
+ `/endpoints/${environmentId}/kubernetes/apis/apps/v1/namespaces/${
+ application.ResourcePool
+ }/${application.ApplicationType.toLowerCase()}s/${application.Name}`
+ );
+ removeApplicationFromStack(application, stacks);
+ } catch (error) {
+ throw parseKubernetesAxiosError(error, 'Unable to remove application');
+ }
+}
+
+async function deletePodApplication(
+ application: ApplicationRowData,
+ stacks: Stack[],
+ environmentId: EnvironmentId
+) {
+ try {
+ await axios.delete(
+ `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${application.ResourcePool}/pods/${application.Name}`
+ );
+ removeApplicationFromStack(application, stacks);
+ } catch (error) {
+ throw parseKubernetesAxiosError(error, 'Unable to remove application');
+ }
+}
+
+async function uninstallHelmApplication(
+ application: ApplicationRowData,
+ environmentId: EnvironmentId
+) {
+ try {
+ await axios.delete(
+ `/endpoints/${environmentId}/kubernetes/helm/${application.Name}`,
+ { params: { namespace: application.ResourcePool } }
+ );
+ } catch (error) {
+ // parseAxiosError, because it's a regular portainer api error
+ throw parseAxiosError(error, 'Unable to remove application');
+ }
+}
+
+// mutate the stacks array to remove the application
+function removeApplicationFromStack(
+ application: ApplicationRowData,
+ stacks: Stack[]
+) {
+ const stack = stacks.find(
+ (stack) =>
+ stack.Name === application.StackName &&
+ stack.ResourcePool === application.ResourcePool
+ );
+ if (stack) {
+ stack.Applications = stack.Applications.filter(
+ (app) => app.Name !== application.Name
+ );
+ }
+}
diff --git a/app/react/kubernetes/applications/autoscaling.service.ts b/app/react/kubernetes/applications/queries/useHorizontalPodAutoScaler.ts
similarity index 64%
rename from app/react/kubernetes/applications/autoscaling.service.ts
rename to app/react/kubernetes/applications/queries/useHorizontalPodAutoScaler.ts
index 37aed7d56..6c3ca1669 100644
--- a/app/react/kubernetes/applications/autoscaling.service.ts
+++ b/app/react/kubernetes/applications/queries/useHorizontalPodAutoScaler.ts
@@ -1,17 +1,14 @@
-import {
- HorizontalPodAutoscaler,
- HorizontalPodAutoscalerList,
-} from 'kubernetes-types/autoscaling/v1';
+import { HorizontalPodAutoscaler } from 'kubernetes-types/autoscaling/v1';
import { useQuery } from '@tanstack/react-query';
import axios from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
-import { withError } from '@/react-tools/react-query';
+import { withGlobalError } from '@/react-tools/react-query';
-import { parseKubernetesAxiosError } from '../axiosError';
+import { parseKubernetesAxiosError } from '../../axiosError';
// when yaml is set to true, the expected return type is a string
-export function useHorizontalAutoScalarQuery<
+export function useHorizontalPodAutoScaler<
T extends HorizontalPodAutoscaler | string = HorizontalPodAutoscaler,
>(
environmentId: EnvironmentId,
@@ -39,28 +36,13 @@ export function useHorizontalAutoScalarQuery<
options
)
: undefined,
- { ...withError('Unable to get horizontal pod autoscaler'), enabled: !!name }
+ {
+ ...withGlobalError('Unable to get horizontal pod autoscaler'),
+ enabled: !!name,
+ }
);
}
-export async function getNamespaceHorizontalPodAutoscalers(
- environmentId: EnvironmentId,
- namespace: string
-) {
- try {
- const { data: autoScalarList } =
- await axios.get(
- `/endpoints/${environmentId}/kubernetes/apis/autoscaling/v1/namespaces/${namespace}/horizontalpodautoscalers`
- );
- return autoScalarList.items;
- } catch (e) {
- throw parseKubernetesAxiosError(
- e,
- 'Unable to retrieve horizontal pod autoscalers'
- );
- }
-}
-
export async function getNamespaceHorizontalPodAutoscaler<
T extends HorizontalPodAutoscaler | string = HorizontalPodAutoscaler,
>(
diff --git a/app/react/kubernetes/applications/queries/usePatchApplicationMutation.ts b/app/react/kubernetes/applications/queries/usePatchApplicationMutation.ts
new file mode 100644
index 000000000..f86847c1c
--- /dev/null
+++ b/app/react/kubernetes/applications/queries/usePatchApplicationMutation.ts
@@ -0,0 +1,155 @@
+import { useMutation } from '@tanstack/react-query';
+import { Deployment, DaemonSet, StatefulSet } from 'kubernetes-types/apps/v1';
+import { Pod } from 'kubernetes-types/core/v1';
+
+import { queryClient } from '@/react-tools/react-query';
+import { EnvironmentId } from '@/react/portainer/environments/types';
+import axios, { parseAxiosError } from '@/portainer/services/axios';
+
+import type { AppKind, ApplicationPatch, Application } from '../types';
+import { parseKubernetesAxiosError } from '../../axiosError';
+
+import { queryKeys } from './query-keys';
+
+// useQuery to patch an application by environmentId, namespace, name and patch payload
+export function usePatchApplicationMutation(
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string
+) {
+ return useMutation(
+ ({
+ appKind,
+ patch,
+ contentType = 'application/json-patch+json',
+ }: {
+ appKind: AppKind;
+ patch: ApplicationPatch;
+ contentType?:
+ | 'application/json-patch+json'
+ | 'application/strategic-merge-patch+json';
+ }) =>
+ patchApplication(
+ environmentId,
+ namespace,
+ appKind,
+ name,
+ patch,
+ contentType
+ ),
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries(
+ queryKeys.application(environmentId, namespace, name)
+ );
+ },
+ // patch application is used for patching and rollbacks, so handle the error where it's used instead of here
+ }
+ );
+}
+
+async function patchApplication(
+ environmentId: EnvironmentId,
+ namespace: string,
+ appKind: AppKind,
+ name: string,
+ patch: ApplicationPatch,
+ contentType: string = 'application/json-patch+json'
+) {
+ switch (appKind) {
+ case 'Deployment':
+ return patchApplicationByKind(
+ environmentId,
+ namespace,
+ appKind,
+ name,
+ patch,
+ contentType
+ );
+ case 'DaemonSet':
+ return patchApplicationByKind(
+ environmentId,
+ namespace,
+ appKind,
+ name,
+ patch,
+ contentType
+ );
+ case 'StatefulSet':
+ return patchApplicationByKind(
+ environmentId,
+ namespace,
+ appKind,
+ name,
+ patch,
+ contentType
+ );
+ case 'Pod':
+ return patchPod(environmentId, namespace, name, patch);
+ default:
+ throw new Error(`Unknown application kind ${appKind}`);
+ }
+}
+
+async function patchApplicationByKind(
+ environmentId: EnvironmentId,
+ namespace: string,
+ appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
+ name: string,
+ patch: ApplicationPatch,
+ contentType = 'application/json-patch+json'
+) {
+ try {
+ const res = await axios.patch(
+ buildUrl(environmentId, namespace, `${appKind}s`, name),
+ patch,
+ {
+ headers: {
+ 'Content-Type': contentType,
+ },
+ }
+ );
+ return res;
+ } catch (e) {
+ throw parseKubernetesAxiosError(e, 'Unable to patch application');
+ }
+}
+
+async function patchPod(
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string,
+ patch: ApplicationPatch
+) {
+ try {
+ return await axios.patch(
+ `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods/${name}`,
+ patch,
+ {
+ headers: {
+ 'Content-Type': 'application/json-patch+json',
+ },
+ }
+ );
+ } catch (e) {
+ throw parseAxiosError(e, 'Unable to update pod');
+ }
+}
+
+function buildUrl(
+ environmentId: EnvironmentId,
+ namespace: string,
+ appKind:
+ | 'Deployments'
+ | 'DaemonSets'
+ | 'StatefulSets'
+ | 'ReplicaSets'
+ | 'ControllerRevisions',
+ name?: string
+) {
+ let baseUrl = `/endpoints/${environmentId}/kubernetes/apis/apps/v1/namespaces/${namespace}/${appKind.toLowerCase()}`;
+ if (name) {
+ baseUrl += `/${name}`;
+ }
+ return baseUrl;
+}
diff --git a/app/react/kubernetes/applications/queries/useRedeployApplicationMutation.ts b/app/react/kubernetes/applications/queries/useRedeployApplicationMutation.ts
new file mode 100644
index 000000000..35c9c2627
--- /dev/null
+++ b/app/react/kubernetes/applications/queries/useRedeployApplicationMutation.ts
@@ -0,0 +1,70 @@
+import { useMutation } from '@tanstack/react-query';
+import { Pod } from 'kubernetes-types/core/v1';
+
+import { queryClient, withGlobalError } from '@/react-tools/react-query';
+import { EnvironmentId } from '@/react/portainer/environments/types';
+import axios from '@/portainer/services/axios';
+
+import { parseKubernetesAxiosError } from '../../axiosError';
+
+import { queryKeys } from './query-keys';
+import { getPods } from './getPods';
+
+// useRedeployApplicationMutation gets all the pods for an application (using the matchLabels field in the labelSelector query param) and then deletes all of them, so that they are recreated
+export function useRedeployApplicationMutation(
+ environmentId: number,
+ namespace: string,
+ name: string
+) {
+ return useMutation(
+ async ({ labelSelector }: { labelSelector: string }) => {
+ try {
+ // get only the pods that match the labelSelector for the application
+ const pods = await getPods(environmentId, namespace, labelSelector);
+ // delete all the pods to redeploy the application
+ await Promise.all(
+ pods.map((pod) => {
+ if (pod?.metadata?.name) {
+ return deletePod(environmentId, namespace, pod.metadata.name);
+ }
+ return Promise.resolve();
+ })
+ );
+ } catch (error) {
+ throw new Error(`Unable to redeploy application: ${error}`);
+ }
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries(
+ queryKeys.application(environmentId, namespace, name)
+ );
+ },
+ ...withGlobalError('Unable to redeploy application'),
+ }
+ );
+}
+
+async function deletePod(
+ environmentId: EnvironmentId,
+ namespace: string,
+ name: string
+) {
+ try {
+ return await axios.delete(buildUrl(environmentId, namespace, name));
+ } catch (e) {
+ throw parseKubernetesAxiosError(e as Error, 'Unable to delete pod');
+ }
+}
+
+function buildUrl(
+ environmentId: EnvironmentId,
+ namespace: string,
+ name?: string
+) {
+ let baseUrl = `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods`;
+ if (name) {
+ baseUrl += `/${name}`;
+ }
+ return baseUrl;
+}
diff --git a/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/NodeApplicationsDatatable.tsx b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/NodeApplicationsDatatable.tsx
index e4b6bfafc..788345d65 100644
--- a/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/NodeApplicationsDatatable.tsx
+++ b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/NodeApplicationsDatatable.tsx
@@ -2,7 +2,7 @@ import { useCurrentStateAndParams } from '@uirouter/react';
import LaptopCode from '@/assets/ico/laptop-code.svg?c';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
-import { useApplications } from '@/react/kubernetes/applications/application.queries';
+import { useApplications } from '@/react/kubernetes/applications/queries/useApplications';
import { Datatable, TableSettingsMenu } from '@@/datatables';
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';