mirror of https://github.com/portainer/portainer
refactor(apps): migrate applications view to react [r8s-124] (#28)
parent
cc75167437
commit
959c527be7
|
@ -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',
|
||||
|
|
|
@ -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;
|
|
@ -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)), [
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
<page-header ng-if="ctrl.state.viewReady" title="'Application list'" breadcrumbs="['Applications']" reload="true"></page-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12" data-cy="k8sApp-appList">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills" ng-if="!ctrl.deploymentOptions.hideStacksFunctionality">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading class="vertical-center"> <pr-icon icon="'box'"></pr-icon> Applications </uib-tab-heading>
|
||||
|
||||
<kubernetes-applications-datatable
|
||||
dataset="ctrl.state.applications"
|
||||
on-refresh="(ctrl.getApplications)"
|
||||
namespaces="ctrl.state.namespaces"
|
||||
namespace="ctrl.state.namespaceName"
|
||||
on-namespace-change="(ctrl.onChangeNamespaceDropdown)"
|
||||
is-loading="ctrl.state.isAppsLoading"
|
||||
on-remove="(ctrl.removeAction)"
|
||||
hide-stacks="ctrl.deploymentOptions.hideStacksFunctionality"
|
||||
>
|
||||
</kubernetes-applications-datatable>
|
||||
</uib-tab>
|
||||
<uib-tab index="2" classes="btn-sm" select="ctrl.selectTab(2)">
|
||||
<uib-tab-heading class="vertical-center"> <pr-icon icon="'list'"></pr-icon> Stacks </uib-tab-heading>
|
||||
|
||||
<kubernetes-applications-stacks-datatable
|
||||
dataset="ctrl.state.stacks"
|
||||
on-refresh="(ctrl.getApplications)"
|
||||
on-remove="(ctrl.removeStacksAction)"
|
||||
namespaces="ctrl.state.namespaces"
|
||||
namespace="ctrl.state.namespaceName"
|
||||
is-loading="ctrl.state.isAppsLoading"
|
||||
on-namespace-change="(ctrl.onChangeNamespaceDropdown)"
|
||||
show-system="ctrl.state.isSystemResources"
|
||||
set-system-resources="(ctrl.setSystemResources)"
|
||||
>
|
||||
</kubernetes-applications-stacks-datatable>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
<kubernetes-applications-datatable
|
||||
ng-if="ctrl.deploymentOptions.hideStacksFunctionality"
|
||||
dataset="ctrl.state.applications"
|
||||
on-refresh="(ctrl.getApplications)"
|
||||
namespaces="ctrl.state.namespaces"
|
||||
namespace="ctrl.state.namespaceName"
|
||||
on-namespace-change="(ctrl.onChangeNamespaceDropdown)"
|
||||
is-loading="ctrl.state.isAppsLoading"
|
||||
on-remove="(ctrl.removeAction)"
|
||||
hide-stacks="ctrl.deploymentOptions.hideStacksFunctionality"
|
||||
>
|
||||
</kubernetes-applications-datatable>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesApplicationsView', {
|
||||
templateUrl: './applications.html',
|
||||
controller: 'KubernetesApplicationsController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
|
@ -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);
|
|
@ -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<string>(
|
||||
useHorizontalPodAutoScaler<string>(
|
||||
environmentId,
|
||||
namespace,
|
||||
autoScalar?.metadata?.name || '',
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {}
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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<Namespace>;
|
||||
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 (
|
||||
<ExpandableDatatable
|
||||
data-cy="k8sApp-appTable"
|
||||
noWidget
|
||||
dataset={filteredApplications ?? []}
|
||||
settingsManager={tableState}
|
||||
columns={columns}
|
||||
title="Applications"
|
||||
titleIcon={BoxIcon}
|
||||
isLoading={applicationsQuery.isLoading}
|
||||
disableSelect={!hasWriteAuthQuery.authorized}
|
||||
disableSelect={!hasWriteAuth}
|
||||
isRowSelectable={(row) =>
|
||||
!isSystemNamespace(row.original.ResourcePool, namespaceListQuery.data)
|
||||
}
|
||||
|
@ -91,25 +92,22 @@ export function ApplicationsDatatable({
|
|||
<SubRow
|
||||
item={row.original}
|
||||
hideStacks={hideStacks}
|
||||
areSecretsRestricted={
|
||||
envQuery.data?.Kubernetes.Configuration.RestrictSecrets || false
|
||||
}
|
||||
areSecretsRestricted={!!restrictSecretsQuery.data}
|
||||
/>
|
||||
)}
|
||||
renderTableActions={(selectedItems) =>
|
||||
hasWriteAuthQuery.authorized && (
|
||||
hasWriteAuth && (
|
||||
<>
|
||||
<DeleteButton
|
||||
data-cy="k8sApp-removeAppButton"
|
||||
disabled={selectedItems.length === 0}
|
||||
isLoading={removeApplicationsMutation.isLoading}
|
||||
confirmMessage="Do you want to remove the selected application(s)?"
|
||||
onConfirmed={() => onRemove(selectedItems)}
|
||||
onConfirmed={() => handleRemoveApplications(selectedItems)}
|
||||
/>
|
||||
|
||||
<AddButton data-cy="k8sApp-addApplicationButton" color="secondary">
|
||||
Add with form
|
||||
</AddButton>
|
||||
|
||||
<CreateFromManifestButton data-cy="k8sApp-deployFromManifestButton" />
|
||||
</>
|
||||
)
|
||||
|
@ -123,18 +121,16 @@ export function ApplicationsDatatable({
|
|||
<div className="w-full">
|
||||
<div className="min-w-[140px] float-right">
|
||||
<NamespaceFilter
|
||||
namespaces={namespaces}
|
||||
value={namespace}
|
||||
onChange={onNamespaceChange}
|
||||
namespaces={namespaceListQuery.data ?? []}
|
||||
value={tableState.namespace}
|
||||
onChange={tableState.setNamespace}
|
||||
showSystem={tableState.showSystemResources}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<SystemResourceDescription
|
||||
showSystemResources={tableState.showSystemResources}
|
||||
/>
|
||||
|
||||
<div className="w-fit">
|
||||
<HelmInsightsBox />
|
||||
</div>
|
||||
|
@ -143,6 +139,14 @@ export function ApplicationsDatatable({
|
|||
}
|
||||
/>
|
||||
);
|
||||
|
||||
function handleRemoveApplications(applications: ApplicationRowData[]) {
|
||||
removeApplicationsMutation.mutate(applications, {
|
||||
onSuccess: () => {
|
||||
router.stateService.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function useApplicationsRowData(
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { ApplicationsDatatable } from './ApplicationsDatatable';
|
|
@ -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<Stack>): void;
|
||||
namespace?: string;
|
||||
namespaces: Array<Namespace>;
|
||||
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 (
|
||||
<ExpandableDatatable
|
||||
|
@ -68,18 +62,17 @@ export function ApplicationsStacksDatatable({
|
|||
isLoading={applicationsQuery.isLoading || namespaceListQuery.isLoading}
|
||||
columns={columns}
|
||||
settingsManager={tableState}
|
||||
disableSelect={!authorized}
|
||||
disableSelect={!hasWriteAuth}
|
||||
renderSubRow={(row) => (
|
||||
<SubRows stack={row.original} span={row.getVisibleCells().length} />
|
||||
)}
|
||||
noWidget
|
||||
description={
|
||||
<div className="w-full">
|
||||
<div className="float-right mr-2 min-w-[140px]">
|
||||
<NamespaceFilter
|
||||
namespaces={namespaces}
|
||||
value={namespace}
|
||||
onChange={onNamespaceChange}
|
||||
namespaces={namespaceListQuery.data ?? []}
|
||||
value={tableState.namespace}
|
||||
onChange={tableState.setNamespace}
|
||||
showSystem={tableState.showSystemResources}
|
||||
/>
|
||||
</div>
|
||||
|
@ -92,7 +85,14 @@ export function ApplicationsStacksDatatable({
|
|||
</div>
|
||||
}
|
||||
renderTableActions={(selectedItems) => (
|
||||
<TableActions selectedItems={selectedItems} onRemove={onRemove} />
|
||||
<Authorized authorizations="K8sApplicationsW">
|
||||
<DeleteButton
|
||||
confirmMessage="Are you sure that you want to remove the selected stack(s) ? This will remove all the applications associated to the stack(s)."
|
||||
disabled={selectedItems.length === 0}
|
||||
onConfirmed={() => handleRemoveStacks(selectedItems)}
|
||||
data-cy="k8sApp-removeStackButton"
|
||||
/>
|
||||
</Authorized>
|
||||
)}
|
||||
renderTableSettings={() => (
|
||||
<TableSettingsMenu>
|
||||
|
@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Stack>;
|
||||
onRemove: (selectedItems: Array<Stack>) => void;
|
||||
}) {
|
||||
return (
|
||||
<Authorized authorizations="K8sApplicationsW">
|
||||
<DeleteButton
|
||||
confirmMessage="Are you sure that you want to remove the selected stack(s) ? This will remove all the applications associated to the stack(s)."
|
||||
disabled={selectedItems.length === 0}
|
||||
onConfirmed={() => onRemove(selectedItems)}
|
||||
data-cy="k8sApp-removeStackButton"
|
||||
/>
|
||||
</Authorized>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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: <ApplicationsDatatable tableState={tableState} />,
|
||||
selectedTabParam: 'applications',
|
||||
},
|
||||
{
|
||||
name: 'Stacks',
|
||||
icon: List,
|
||||
widget: <ApplicationsStacksDatatable tableState={tableState} />,
|
||||
selectedTabParam: 'stacks',
|
||||
},
|
||||
];
|
||||
|
||||
const currentTabIndex = findSelectedTabIndex(
|
||||
useCurrentStateAndParams(),
|
||||
tabs
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Application list" breadcrumbs="Applications" reload />
|
||||
{hideStacks ? (
|
||||
<ApplicationsDatatable tableState={tableState} hideStacks />
|
||||
) : (
|
||||
<>
|
||||
<WidgetTabs tabs={tabs} currentTabIndex={currentTabIndex} />
|
||||
<div className="content">{tabs[currentTabIndex].widget}</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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<T extends NamespaceSelectSettings>(
|
||||
set: ZustandSetFunc<T>,
|
||||
namespace = 'default'
|
||||
): NamespaceSelectSettings {
|
||||
return {
|
||||
namespace,
|
||||
setNamespace: (namespace: string) => set((s) => ({ ...s, namespace })),
|
||||
};
|
||||
}
|
||||
|
||||
export function createStore<T extends ApplicationsTableSettings>(
|
||||
storageKey: string,
|
||||
initialSortBy?: string | { id: string; desc: boolean },
|
||||
create: (
|
||||
set: ZustandSetFunc<T>
|
||||
) => Omit<T, keyof ApplicationsTableSettings> = () => ({}) as T
|
||||
) {
|
||||
return createPersistedStore<T>(
|
||||
storageKey,
|
||||
initialSortBy,
|
||||
(set) =>
|
||||
({
|
||||
...refreshableSettings(set),
|
||||
...systemResourcesSettings(set),
|
||||
...namespaceSelectSettings(set),
|
||||
...create(set),
|
||||
}) as T
|
||||
);
|
||||
}
|
||||
|
||||
export function useKubeAppsTableStore<T extends ApplicationsTableSettings>(
|
||||
...args: Parameters<typeof createStore<T>>
|
||||
) {
|
||||
const [store] = useState(() => createStore(...args));
|
||||
return useTableState(store, args[0]);
|
||||
}
|
|
@ -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<T extends Application | string = Application>(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
name: string,
|
||||
appKind?: AppKind,
|
||||
options?: { autoRefreshRate?: number; yaml?: boolean }
|
||||
): UseQueryResult<T> {
|
||||
return useQuery(
|
||||
queryKeys.application(environmentId, namespace, name, options?.yaml),
|
||||
() =>
|
||||
getApplication<T>(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>('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>('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<PodList>(
|
||||
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods`,
|
||||
{
|
||||
params: {
|
||||
labelSelector,
|
||||
},
|
||||
}
|
||||
);
|
||||
const items = (data.items || []).map(
|
||||
(pod) =>
|
||||
<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<K8sApplication[]>(
|
||||
`/kubernetes/${environmentId}/applications`,
|
||||
{ params }
|
||||
);
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e, 'Unable to retrieve applications');
|
||||
}
|
||||
}
|
|
@ -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<T>(
|
||||
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<Deployment>(
|
||||
environmentId,
|
||||
namespace,
|
||||
'Deployment',
|
||||
name,
|
||||
yaml
|
||||
),
|
||||
getApplicationByKind<DaemonSet>(
|
||||
environmentId,
|
||||
namespace,
|
||||
'DaemonSet',
|
||||
name,
|
||||
yaml
|
||||
),
|
||||
getApplicationByKind<StatefulSet>(
|
||||
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<Deployment>(
|
||||
environmentId,
|
||||
namespace,
|
||||
appKind,
|
||||
name,
|
||||
patch,
|
||||
contentType
|
||||
);
|
||||
case 'DaemonSet':
|
||||
return patchApplicationByKind<DaemonSet>(
|
||||
environmentId,
|
||||
namespace,
|
||||
appKind,
|
||||
name,
|
||||
patch,
|
||||
contentType
|
||||
);
|
||||
case 'StatefulSet':
|
||||
return patchApplicationByKind<StatefulSet>(
|
||||
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<T extends Application>(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
|
||||
name: string,
|
||||
patch: ApplicationPatch,
|
||||
contentType = 'application/json-patch+json'
|
||||
) {
|
||||
try {
|
||||
const res = await axios.patch<T>(
|
||||
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<T>(
|
||||
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<ReplicaSetList>(
|
||||
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<ControllerRevisionList>(
|
||||
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;
|
||||
}
|
|
@ -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<T extends Pod | string = Pod>(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
name: string,
|
||||
yaml?: boolean
|
||||
) {
|
||||
try {
|
||||
const { data } = await axios.get<T>(
|
||||
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<Pod>(
|
||||
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<Pod>(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;
|
||||
}
|
|
@ -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<PodList>(
|
||||
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods`,
|
||||
{
|
||||
params: {
|
||||
labelSelector,
|
||||
},
|
||||
}
|
||||
);
|
||||
const items = (data.items || []).map(
|
||||
(pod) =>
|
||||
<Pod>{
|
||||
...pod,
|
||||
kind: 'Pod',
|
||||
apiVersion: data.apiVersion,
|
||||
}
|
||||
);
|
||||
return items;
|
||||
} catch (e) {
|
||||
throw parseKubernetesAxiosError(
|
||||
e,
|
||||
`Unable to retrieve Pods in namespace '${namespace}'`
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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<T extends Application | string = Application>(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
name: string,
|
||||
appKind?: AppKind,
|
||||
options?: { autoRefreshRate?: number; yaml?: boolean }
|
||||
): UseQueryResult<T> {
|
||||
return useQuery(
|
||||
queryKeys.application(environmentId, namespace, name, options?.yaml),
|
||||
() =>
|
||||
getApplication<T>(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<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<T>(
|
||||
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<Deployment>(
|
||||
environmentId,
|
||||
namespace,
|
||||
'Deployment',
|
||||
name,
|
||||
yaml
|
||||
),
|
||||
getApplicationByKind<DaemonSet>(
|
||||
environmentId,
|
||||
namespace,
|
||||
'DaemonSet',
|
||||
name,
|
||||
yaml
|
||||
),
|
||||
getApplicationByKind<StatefulSet>(
|
||||
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<T extends Pod | string = Pod>(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
name: string,
|
||||
yaml?: boolean
|
||||
) {
|
||||
try {
|
||||
const { data } = await axios.get<T>(
|
||||
`/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<T>(
|
||||
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;
|
||||
}
|
|
@ -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<HorizontalPodAutoscalerList>(
|
||||
`/endpoints/${environmentId}/kubernetes/apis/autoscaling/v1/namespaces/${namespace}/horizontalpodautoscalers`
|
||||
);
|
||||
return autoScalarList.items;
|
||||
} catch (e) {
|
||||
throw parseKubernetesAxiosError(
|
||||
e,
|
||||
'Unable to retrieve horizontal pod autoscalers'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>('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;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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<ReplicaSetList>(
|
||||
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<ControllerRevisionList>(
|
||||
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;
|
||||
}
|
|
@ -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>('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,
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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<Application[]>(
|
||||
`/kubernetes/${environmentId}/applications`,
|
||||
{ params }
|
||||
);
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e, 'Unable to retrieve applications');
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<HorizontalPodAutoscalerList>(
|
||||
`/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,
|
||||
>(
|
|
@ -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<Deployment>(
|
||||
environmentId,
|
||||
namespace,
|
||||
appKind,
|
||||
name,
|
||||
patch,
|
||||
contentType
|
||||
);
|
||||
case 'DaemonSet':
|
||||
return patchApplicationByKind<DaemonSet>(
|
||||
environmentId,
|
||||
namespace,
|
||||
appKind,
|
||||
name,
|
||||
patch,
|
||||
contentType
|
||||
);
|
||||
case 'StatefulSet':
|
||||
return patchApplicationByKind<StatefulSet>(
|
||||
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<T extends Application>(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
|
||||
name: string,
|
||||
patch: ApplicationPatch,
|
||||
contentType = 'application/json-patch+json'
|
||||
) {
|
||||
try {
|
||||
const res = await axios.patch<T>(
|
||||
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<Pod>(
|
||||
`/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;
|
||||
}
|
|
@ -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<Pod>(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;
|
||||
}
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue