mirror of https://github.com/portainer/portainer
feat(app): migrate app parent view to react [EE-5361] (#10086)
Co-authored-by: testa113 <testa113>pull/10191/head
parent
531f88b947
commit
841ca1ebd4
@ -1,83 +0,0 @@
|
||||
<page-header
|
||||
ng-if="ctrl.state.viewReady"
|
||||
title="'Application details'"
|
||||
breadcrumbs="[
|
||||
{ label:'Namespaces', link:'kubernetes.resourcePools' },
|
||||
{
|
||||
label:ctrl.application.ResourcePool,
|
||||
link: 'kubernetes.resourcePools.resourcePool',
|
||||
linkParams:{ id: ctrl.application.ResourcePool }
|
||||
},
|
||||
{ label:'Applications', link:'kubernetes.applications' },
|
||||
ctrl.application.Name
|
||||
]"
|
||||
reload="true"
|
||||
>
|
||||
</page-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row kubernetes-application">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading> <pr-icon icon="'svg-laptopcode'" class-name="'mr-1'"></pr-icon> Application </uib-tab-heading>
|
||||
<application-summary-widget></application-summary-widget>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||
<uib-tab-heading>
|
||||
<pr-icon icon="'minimize-2'"></pr-icon> Placement
|
||||
<div ng-if="ctrl.state.placementWarning" class="vertical-center">
|
||||
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
|
||||
warning
|
||||
</div>
|
||||
</uib-tab-heading>
|
||||
<div class="small text-muted vertical-center" style="padding: 20px">
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
The placement component helps you understand whether or not this application can be deployed on a specific node.
|
||||
</div>
|
||||
|
||||
<kubernetes-application-placements-datatable
|
||||
ng-if="ctrl.placements"
|
||||
dataset="ctrl.placements"
|
||||
on-refresh="(ctrl.getApplication)"
|
||||
></kubernetes-application-placements-datatable>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="2" classes="btn-sm" select="ctrl.selectTab(2)">
|
||||
<uib-tab-heading>
|
||||
<pr-icon icon="'history'"></pr-icon> Events
|
||||
<div ng-if="ctrl.hasEventWarnings()" class="vertical-center">
|
||||
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
|
||||
{{ ctrl.state.eventWarningCount }} warning(s)
|
||||
</div>
|
||||
</uib-tab-heading>
|
||||
<application-events-datatable />
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="3" ng-if="ctrl.application.Yaml" select="ctrl.showEditor()" classes="btn-sm">
|
||||
<uib-tab-heading> <pr-icon icon="'code'"></pr-icon> YAML </uib-tab-heading>
|
||||
<div class="px-5" ng-if="ctrl.state.showEditorTab">
|
||||
<kube-yaml-inspector identifier="'application-yaml'" data="ctrl.application.Yaml" />
|
||||
</div>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<application-details-widget></application-details-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<application-containers-datatable />
|
||||
</div>
|
||||
</div>
|
@ -1,9 +0,0 @@
|
||||
angular.module('portainer.kubernetes').component('kubernetesApplicationView', {
|
||||
templateUrl: './application.html',
|
||||
controller: 'KubernetesApplicationController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
@ -1,266 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import {
|
||||
KubernetesApplicationDataAccessPolicies,
|
||||
KubernetesApplicationDeploymentTypes,
|
||||
KubernetesApplicationTypes,
|
||||
KubernetesDeploymentTypes,
|
||||
} from 'Kubernetes/models/application/models';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
|
||||
import { KubernetesPodContainerTypes } from 'Kubernetes/pod/models/index';
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
|
||||
function computeTolerations(nodes, application) {
|
||||
const pod = application.Pods[0];
|
||||
_.forEach(nodes, (n) => {
|
||||
n.AcceptsApplication = true;
|
||||
n.Expanded = false;
|
||||
if (!pod) {
|
||||
return;
|
||||
}
|
||||
n.UnmetTaints = [];
|
||||
_.forEach(n.Taints, (t) => {
|
||||
const matchKeyMatchValueMatchEffect = _.find(pod.Tolerations, { Key: t.Key, Operator: 'Equal', Value: t.Value, Effect: t.Effect });
|
||||
const matchKeyAnyValueMatchEffect = _.find(pod.Tolerations, { Key: t.Key, Operator: 'Exists', Effect: t.Effect });
|
||||
const matchKeyMatchValueAnyEffect = _.find(pod.Tolerations, { Key: t.Key, Operator: 'Equal', Value: t.Value, Effect: '' });
|
||||
const matchKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: t.Key, Operator: 'Exists', Effect: '' });
|
||||
const anyKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: '', Operator: 'Exists', Effect: '' });
|
||||
|
||||
if (!matchKeyMatchValueMatchEffect && !matchKeyAnyValueMatchEffect && !matchKeyMatchValueAnyEffect && !matchKeyAnyValueAnyEffect && !anyKeyAnyValueAnyEffect) {
|
||||
n.AcceptsApplication = false;
|
||||
n.UnmetTaints.push(t);
|
||||
} else {
|
||||
n.AcceptsApplication = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
// For node requirement format depending on operator value
|
||||
// see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#nodeselectorrequirement-v1-core
|
||||
// Some operators require empty "values" field, some only one element in "values" field, etc
|
||||
|
||||
function computeAffinities(nodes, application) {
|
||||
if (!application.Pods || application.Pods.length === 0) {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
const pod = application.Pods[0];
|
||||
_.forEach(nodes, (n) => {
|
||||
if (pod.NodeSelector) {
|
||||
const patch = JsonPatch.compare(n.Labels, pod.NodeSelector);
|
||||
_.remove(patch, { op: 'remove' });
|
||||
n.UnmatchedNodeSelectorLabels = _.map(patch, (i) => {
|
||||
return { key: _.trimStart(i.path, '/'), value: i.value };
|
||||
});
|
||||
if (n.UnmatchedNodeSelectorLabels.length) {
|
||||
n.AcceptsApplication = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (pod.Affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution) {
|
||||
const unmatchedTerms = _.map(pod.Affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, (t) => {
|
||||
const unmatchedExpressions = _.map(t.matchExpressions, (e) => {
|
||||
const exists = {}.hasOwnProperty.call(n.Labels, e.key);
|
||||
const isIn = exists && _.includes(e.values, n.Labels[e.key]);
|
||||
if (
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.EXISTS && exists) ||
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST && !exists) ||
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN && isIn) ||
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN && !isIn) ||
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN && exists && parseInt(n.Labels[e.key], 10) > parseInt(e.values[0], 10)) ||
|
||||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN && exists && parseInt(n.Labels[e.key], 10) < parseInt(e.values[0], 10))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return e;
|
||||
});
|
||||
return _.without(unmatchedExpressions, undefined);
|
||||
});
|
||||
_.remove(unmatchedTerms, (i) => i.length === 0);
|
||||
n.UnmatchedNodeAffinities = unmatchedTerms;
|
||||
if (n.UnmatchedNodeAffinities.length) {
|
||||
n.AcceptsApplication = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function computePlacements(nodes, application) {
|
||||
nodes = computeTolerations(nodes, application);
|
||||
nodes = computeAffinities(nodes, application);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
class KubernetesApplicationController {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$async,
|
||||
$state,
|
||||
clipboard,
|
||||
Notifications,
|
||||
LocalStorage,
|
||||
KubernetesResourcePoolService,
|
||||
KubernetesApplicationService,
|
||||
KubernetesEventService,
|
||||
KubernetesStackService,
|
||||
KubernetesPodService,
|
||||
KubernetesNodeService,
|
||||
StackService
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.clipboard = clipboard;
|
||||
this.Notifications = Notifications;
|
||||
this.LocalStorage = LocalStorage;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.StackService = StackService;
|
||||
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.KubernetesEventService = KubernetesEventService;
|
||||
this.KubernetesStackService = KubernetesStackService;
|
||||
this.KubernetesPodService = KubernetesPodService;
|
||||
this.KubernetesNodeService = KubernetesNodeService;
|
||||
|
||||
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||
this.KubernetesApplicationTypes = KubernetesApplicationTypes;
|
||||
this.KubernetesDeploymentTypes = KubernetesDeploymentTypes;
|
||||
|
||||
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
||||
this.KubernetesServiceTypes = KubernetesServiceTypes;
|
||||
this.KubernetesPodContainerTypes = KubernetesPodContainerTypes;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getApplication = this.getApplication.bind(this);
|
||||
this.getApplicationAsync = this.getApplicationAsync.bind(this);
|
||||
this.getEvents = this.getEvents.bind(this);
|
||||
this.getEventsAsync = this.getEventsAsync.bind(this);
|
||||
}
|
||||
|
||||
selectTab(index) {
|
||||
this.LocalStorage.storeActiveTab('application', index);
|
||||
}
|
||||
|
||||
showEditor() {
|
||||
this.state.showEditorTab = true;
|
||||
this.selectTab(3);
|
||||
}
|
||||
|
||||
isSystemNamespace() {
|
||||
return KubernetesNamespaceHelper.isSystemNamespace(this.application.ResourcePool);
|
||||
}
|
||||
|
||||
hasEventWarnings() {
|
||||
return this.state.eventWarningCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* EVENTS
|
||||
*/
|
||||
async getEventsAsync() {
|
||||
try {
|
||||
this.state.eventsLoading = true;
|
||||
const events = await this.KubernetesEventService.get(this.state.params.namespace);
|
||||
this.events = _.filter(
|
||||
events,
|
||||
(event) =>
|
||||
event.Involved.uid === this.application.Id ||
|
||||
event.Involved.uid === this.application.ServiceId ||
|
||||
_.find(this.application.Pods, (pod) => pod.Id === event.Involved.uid) !== undefined
|
||||
);
|
||||
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application related events');
|
||||
} finally {
|
||||
this.state.eventsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
return this.$async(this.getEventsAsync);
|
||||
}
|
||||
|
||||
/**
|
||||
* APPLICATION
|
||||
*/
|
||||
async getApplicationAsync() {
|
||||
try {
|
||||
this.state.dataLoading = true;
|
||||
const [application, nodes] = await Promise.all([
|
||||
this.KubernetesApplicationService.get(this.state.params.namespace, this.state.params.name),
|
||||
this.KubernetesNodeService.get(),
|
||||
]);
|
||||
this.application = application;
|
||||
|
||||
this.placements = computePlacements(nodes, this.application);
|
||||
this.state.placementWarning = _.find(this.placements, { AcceptsApplication: true }) ? false : true;
|
||||
|
||||
if (application.StackId) {
|
||||
const file = await this.StackService.getStackFile(application.StackId);
|
||||
this.stackFileContent = file;
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
||||
} finally {
|
||||
this.state.dataLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
return this.$async(this.getApplicationAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.limitedFeature = FeatureId.K8S_ROLLING_RESTART;
|
||||
|
||||
this.state = {
|
||||
activeTab: 0,
|
||||
currentName: this.$state.$current.name,
|
||||
showEditorTab: false,
|
||||
DisplayedPanel: 'pods',
|
||||
eventsLoading: true,
|
||||
dataLoading: true,
|
||||
viewReady: false,
|
||||
params: {
|
||||
namespace: this.$transition$.params().namespace,
|
||||
name: this.$transition$.params().name,
|
||||
},
|
||||
appType: this.KubernetesDeploymentTypes.APPLICATION_FORM,
|
||||
eventWarningCount: 0,
|
||||
placementWarning: false,
|
||||
expandedNote: false,
|
||||
publicUrl: this.endpoint.PublicURL,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('application');
|
||||
|
||||
this.formValues = {
|
||||
Note: '',
|
||||
SelectedRevision: undefined,
|
||||
};
|
||||
|
||||
await this.getApplication();
|
||||
await this.getEvents();
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
if (this.state.currentName !== this.$state.$current.name) {
|
||||
this.LocalStorage.storeActiveTab('application', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesApplicationController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesApplicationController', KubernetesApplicationController);
|
@ -0,0 +1,89 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { ChevronUp, ChevronRight, Edit } from 'lucide-react';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
|
||||
export type Props = {
|
||||
onChange: (value: string) => void;
|
||||
defaultIsOpen?: boolean;
|
||||
value?: string;
|
||||
labelClass?: string;
|
||||
inputClass?: string;
|
||||
isRequired?: boolean;
|
||||
minLength?: number;
|
||||
isExpandable?: boolean;
|
||||
};
|
||||
|
||||
export function Note({
|
||||
onChange,
|
||||
defaultIsOpen,
|
||||
value,
|
||||
labelClass = 'col-sm-12 mb-2',
|
||||
inputClass = 'col-sm-12',
|
||||
isRequired,
|
||||
minLength,
|
||||
isExpandable,
|
||||
}: Props) {
|
||||
const [isNoteOpen, setIsNoteOpen] = useState(defaultIsOpen || isRequired);
|
||||
|
||||
useEffect(() => {
|
||||
setIsNoteOpen(defaultIsOpen || isRequired);
|
||||
}, [defaultIsOpen, isRequired]);
|
||||
|
||||
let error = '';
|
||||
if (isRequired && minLength && (!value || value.length < minLength)) {
|
||||
error = `You have entered ${value ? value.length : 0} of the ${minLength} ${
|
||||
minLength === 1 ? 'character' : 'characters'
|
||||
} minimum required.`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-group">
|
||||
<div className={clsx('vertical-center', labelClass)}>
|
||||
{isExpandable && (
|
||||
<Button
|
||||
size="small"
|
||||
type="button"
|
||||
color="none"
|
||||
data-cy="k8sAppDetail-expandNoteButton"
|
||||
onClick={() => setIsNoteOpen(!isNoteOpen)}
|
||||
className="!m-0 !p-0"
|
||||
>
|
||||
{isNoteOpen ? <ChevronUp /> : <ChevronRight />} <Edit />
|
||||
<span className={isRequired ? 'required' : ''}>Note</span>
|
||||
</Button>
|
||||
)}
|
||||
{!isExpandable && (
|
||||
<span
|
||||
className={clsx(
|
||||
'control-label text-left',
|
||||
isRequired ? 'required' : ''
|
||||
)}
|
||||
>
|
||||
Note
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isNoteOpen && (
|
||||
<div className={inputClass}>
|
||||
<textarea
|
||||
className="form-control resize-y"
|
||||
name="application_note"
|
||||
id="application_note"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
rows={5}
|
||||
placeholder="Enter a note about this application..."
|
||||
minLength={minLength}
|
||||
/>
|
||||
{error && (
|
||||
<FormError className="error-inline mt-2">{error}</FormError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { Note, type Props } from './Note';
|
@ -0,0 +1,44 @@
|
||||
import { InlineLoader } from '@@/InlineLoader';
|
||||
import { Widget } from '@@/Widget/Widget';
|
||||
import { WidgetBody } from '@@/Widget';
|
||||
|
||||
import { YAMLInspector } from '../../../components/YAMLInspector';
|
||||
|
||||
import { useApplicationYAML } from './useApplicationYAML';
|
||||
|
||||
// the yaml currently has the yaml from the app, related services and horizontal pod autoscalers
|
||||
// TODO: this could be extended to include other related resources like ingresses, etc.
|
||||
export function ApplicationYAMLEditor() {
|
||||
const { fullApplicationYaml, isApplicationYAMLLoading } =
|
||||
useApplicationYAML();
|
||||
|
||||
if (isApplicationYAMLLoading) {
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<WidgetBody>
|
||||
<InlineLoader>Loading application YAML...</InlineLoader>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<WidgetBody>
|
||||
<YAMLInspector
|
||||
identifier="application-yaml"
|
||||
data={fullApplicationYaml}
|
||||
hideMessage
|
||||
/>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useServicesQuery } from '@/react/kubernetes/services/service';
|
||||
|
||||
import {
|
||||
useApplication,
|
||||
useApplicationHorizontalPodAutoscaler,
|
||||
useApplicationServices,
|
||||
} from '../../application.queries';
|
||||
import { useHorizontalAutoScalarQuery } from '../../autoscaling.service';
|
||||
|
||||
export function useApplicationYAML() {
|
||||
const {
|
||||
params: {
|
||||
namespace,
|
||||
name,
|
||||
'resource-type': resourceType,
|
||||
endpointId: environmentId,
|
||||
},
|
||||
} = useCurrentStateAndParams();
|
||||
// find the application and the yaml for it
|
||||
const { data: application, ...applicationQuery } = useApplication(
|
||||
environmentId,
|
||||
namespace,
|
||||
name,
|
||||
resourceType
|
||||
);
|
||||
const { data: applicationYAML, ...applicationYAMLQuery } =
|
||||
useApplication<string>(environmentId, namespace, name, resourceType, {
|
||||
yaml: true,
|
||||
});
|
||||
|
||||
// find the matching services, then get the yaml for them
|
||||
const { data: services, ...servicesQuery } = useApplicationServices(
|
||||
environmentId,
|
||||
namespace,
|
||||
name,
|
||||
application
|
||||
);
|
||||
const serviceNames =
|
||||
services?.flatMap((service) => service.metadata?.name || []) || [];
|
||||
const { data: servicesYAML, ...servicesYAMLQuery } = useServicesQuery<string>(
|
||||
environmentId,
|
||||
namespace,
|
||||
serviceNames,
|
||||
{ yaml: true }
|
||||
);
|
||||
|
||||
// find the matching autoscalar, then get the yaml for it
|
||||
const { data: autoScalar, ...autoScalarsQuery } =
|
||||
useApplicationHorizontalPodAutoscaler(
|
||||
environmentId,
|
||||
namespace,
|
||||
name,
|
||||
application
|
||||
);
|
||||
const { data: autoScalarYAML, ...autoScalarYAMLQuery } =
|
||||
useHorizontalAutoScalarQuery<string>(
|
||||
environmentId,
|
||||
namespace,
|
||||
autoScalar?.metadata?.name || '',
|
||||
{ yaml: true }
|
||||
);
|
||||
|
||||
const fullApplicationYaml = useMemo(() => {
|
||||
const yamlArray = [
|
||||
applicationYAML,
|
||||
...(servicesYAML || []),
|
||||
autoScalarYAML,
|
||||
].flatMap((yaml) => yaml || []);
|
||||
|
||||
const yamlString = yamlArray.join('\n---\n');
|
||||
return yamlString;
|
||||
}, [applicationYAML, autoScalarYAML, servicesYAML]);
|
||||
|
||||
const isApplicationYAMLLoading =
|
||||
applicationQuery.isLoading ||
|
||||
servicesQuery.isLoading ||
|
||||
autoScalarsQuery.isLoading ||
|
||||
applicationYAMLQuery.isLoading ||
|
||||
servicesYAMLQuery.isLoading ||
|
||||
autoScalarYAMLQuery.isLoading;
|
||||
|
||||
return { fullApplicationYaml, isApplicationYAMLLoading };
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
import { AlertTriangle, Code, History, Minimize2 } from 'lucide-react';
|
||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
|
||||
import LaptopCode from '@/assets/ico/laptop-code.svg?c';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Tab, WidgetTabs, findSelectedTabIndex } from '@@/Widget/WidgetTabs';
|
||||
import { Icon } from '@@/Icon';
|
||||
import { Badge } from '@@/Badge';
|
||||
|
||||
import { EventsDatatable } from '../../components/KubernetesEventsDatatable';
|
||||
|
||||
import {
|
||||
PlacementsDatatable,
|
||||
usePlacementTableData,
|
||||
usePlacementTableState,
|
||||
} from './PlacementsDatatable';
|
||||
import { ApplicationDetailsWidget } from './ApplicationDetailsWidget';
|
||||
import { ApplicationSummaryWidget } from './ApplicationSummaryWidget';
|
||||
import { ApplicationContainersDatatable } from './ApplicationContainersDatatable';
|
||||
import {
|
||||
useApplicationEventsTableData,
|
||||
useApplicationEventsTableState,
|
||||
} from './useApplicationEventsTableData';
|
||||
import { ApplicationYAMLEditor } from './AppYAMLEditor/ApplicationYAMLEditor';
|
||||
import { useApplicationYAML } from './AppYAMLEditor/useApplicationYAML';
|
||||
|
||||
export function ApplicationDetailsView() {
|
||||
const stateAndParams = useCurrentStateAndParams();
|
||||
const {
|
||||
params: { namespace, name },
|
||||
} = stateAndParams;
|
||||
|
||||
// placements table data
|
||||
const { placementsData, isPlacementsTableLoading, hasPlacementWarning } =
|
||||
usePlacementTableData();
|
||||
const placementsTableState = usePlacementTableState();
|
||||
|
||||
// events table data
|
||||
const { appEventsData, appEventWarningCount, isAppEventsTableLoading } =
|
||||
useApplicationEventsTableData();
|
||||
const appEventsTableState = useApplicationEventsTableState();
|
||||
|
||||
// load app yaml data early to load from cache later
|
||||
useApplicationYAML();
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
name: 'Application',
|
||||
icon: LaptopCode,
|
||||
widget: <ApplicationSummaryWidget />,
|
||||
selectedTabParam: 'application',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex items-center gap-x-2">
|
||||
Placement
|
||||
{hasPlacementWarning && (
|
||||
<Badge type="warnSecondary">
|
||||
<Icon icon={AlertTriangle} className="!mr-1" />1
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
icon: Minimize2,
|
||||
widget: (
|
||||
<PlacementsDatatable
|
||||
hasPlacementWarning={hasPlacementWarning}
|
||||
tableState={placementsTableState}
|
||||
dataset={placementsData}
|
||||
isLoading={isPlacementsTableLoading}
|
||||
/>
|
||||
),
|
||||
selectedTabParam: 'placement',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex items-center gap-x-2">
|
||||
Events
|
||||
{appEventWarningCount >= 1 && (
|
||||
<Badge type="warnSecondary">
|
||||
<Icon icon={AlertTriangle} className="!mr-1" />
|
||||
{appEventWarningCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
icon: History,
|
||||
widget: (
|
||||
<EventsDatatable
|
||||
dataset={appEventsData}
|
||||
tableState={appEventsTableState}
|
||||
isLoading={isAppEventsTableLoading}
|
||||
data-cy="k8sAppDetail-eventsTable"
|
||||
/>
|
||||
),
|
||||
selectedTabParam: 'events',
|
||||
},
|
||||
{
|
||||
name: 'YAML',
|
||||
icon: Code,
|
||||
widget: <ApplicationYAMLEditor />,
|
||||
selectedTabParam: 'YAML',
|
||||
},
|
||||
];
|
||||
const currentTabIndex = findSelectedTabIndex(stateAndParams, tabs);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Application details"
|
||||
breadcrumbs={[
|
||||
{ label: 'Namespaces', link: 'kubernetes.resourcePools' },
|
||||
{
|
||||
label: namespace,
|
||||
link: 'kubernetes.resourcePools.resourcePool',
|
||||
linkParams: { id: namespace },
|
||||
},
|
||||
{ label: 'Applications', link: 'kubernetes.applications' },
|
||||
name,
|
||||
]}
|
||||
reload
|
||||
/>
|
||||
<>
|
||||
<WidgetTabs tabs={tabs} currentTabIndex={currentTabIndex} />
|
||||
{tabs[currentTabIndex].widget}
|
||||
<ApplicationDetailsWidget />
|
||||
<ApplicationContainersDatatable />
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { NodePlacementRowData } from '../../types';
|
||||
|
||||
export const columnHelper = createColumnHelper<NodePlacementRowData>();
|
@ -1,14 +1,14 @@
|
||||
import { buildExpandColumn } from '@@/datatables/expand-column';
|
||||
|
||||
import { Node } from '../../types';
|
||||
import { NodePlacementRowData } from '../../types';
|
||||
|
||||
import { columnHelper } from './helper';
|
||||
import { status } from './status';
|
||||
|
||||
export const columns = [
|
||||
buildExpandColumn<Node>(),
|
||||
buildExpandColumn<NodePlacementRowData>(),
|
||||
status,
|
||||
columnHelper.accessor('Name', {
|
||||
columnHelper.accessor('name', {
|
||||
header: 'Node',
|
||||
id: 'node',
|
||||
}),
|
@ -0,0 +1,5 @@
|
||||
export { PlacementsDatatable } from './PlacementsDatatable';
|
||||
export {
|
||||
usePlacementTableState,
|
||||
usePlacementTableData,
|
||||
} from './usePlacementTableData';
|
@ -0,0 +1,249 @@
|
||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
import { useMemo } from 'react';
|
||||
import { Pod, Taint, Node } from 'kubernetes-types/core/v1';
|
||||
import _ from 'lodash';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
import { useNodesQuery } from '@/react/kubernetes/cluster/HomeView/nodes.service';
|
||||
|
||||
import {
|
||||
BasicTableSettings,
|
||||
RefreshableTableSettings,
|
||||
createPersistedStore,
|
||||
refreshableSettings,
|
||||
} from '@@/datatables/types';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
|
||||
import { useApplication, useApplicationPods } from '../../application.queries';
|
||||
import { NodePlacementRowData } from '../types';
|
||||
|
||||
interface TableSettings extends BasicTableSettings, RefreshableTableSettings {}
|
||||
|
||||
function createStore(storageKey: string) {
|
||||
return createPersistedStore<TableSettings>(storageKey, 'node', (set) => ({
|
||||
...refreshableSettings(set),
|
||||
}));
|
||||
}
|
||||
|
||||
const storageKey = 'kubernetes.application.placements';
|
||||
const placementsSettingsStore = createStore(storageKey);
|
||||
|
||||
export function usePlacementTableState() {
|
||||
return useTableState(placementsSettingsStore, storageKey);
|
||||
}
|
||||
|
||||
export function usePlacementTableData() {
|
||||
const placementsTableState = usePlacementTableState();
|
||||
const autoRefreshRate = placementsTableState.autoRefreshRate * 1000; // ms to seconds
|
||||
|
||||
const stateAndParams = useCurrentStateAndParams();
|
||||
const {
|
||||
params: {
|
||||
namespace,
|
||||
name,
|
||||
'resource-type': resourceType,
|
||||
endpointId: environmentId,
|
||||
},
|
||||
} = stateAndParams;
|
||||
const { data: application, ...applicationQuery } = useApplication(
|
||||
environmentId,
|
||||
namespace,
|
||||
name,
|
||||
resourceType,
|
||||
{ autoRefreshRate }
|
||||
);
|
||||
const { data: pods, ...podsQuery } = useApplicationPods(
|
||||
environmentId,
|
||||
namespace,
|
||||
name,
|
||||
application,
|
||||
{ autoRefreshRate }
|
||||
);
|
||||
const { data: nodes, ...nodesQuery } = useNodesQuery(environmentId, {
|
||||
autoRefreshRate,
|
||||
});
|
||||
|
||||
const placementsData = useMemo(
|
||||
() => (nodes && pods ? computePlacements(nodes, pods) : []),
|
||||
[nodes, pods]
|
||||
);
|
||||
|
||||
const isPlacementsTableLoading =
|
||||
applicationQuery.isLoading || nodesQuery.isLoading || podsQuery.isLoading;
|
||||
|
||||
const hasPlacementWarning = useMemo(() => {
|
||||
const notAllowedOnEveryNode = placementsData.every(
|
||||
(nodePlacement) => !nodePlacement.acceptsApplication
|
||||
);
|
||||
return !isPlacementsTableLoading && notAllowedOnEveryNode;
|
||||
}, [isPlacementsTableLoading, placementsData]);
|
||||
|
||||
return {
|
||||
placementsData,
|
||||
isPlacementsTableLoading,
|
||||
hasPlacementWarning,
|
||||
};
|
||||
}
|
||||
|
||||
export function computePlacements(
|
||||
nodes: Node[],
|
||||
pods: Pod[]
|
||||
): NodePlacementRowData[] {
|
||||
const pod = pods?.[0];
|
||||
if (!pod) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const placementDataFromTolerations: NodePlacementRowData[] =
|
||||
computeTolerations(nodes, pod);
|
||||
const placementDataFromAffinities: NodePlacementRowData[] = computeAffinities(
|
||||
nodes,
|
||||
placementDataFromTolerations,
|
||||
pod
|
||||
);
|
||||
return placementDataFromAffinities;
|
||||
}
|
||||
|
||||
function computeTolerations(nodes: Node[], pod: Pod): NodePlacementRowData[] {
|
||||
const tolerations = pod.spec?.tolerations || [];
|
||||
const nodePlacements: NodePlacementRowData[] = nodes.map((node) => {
|
||||
let acceptsApplication = true;
|
||||
const unmetTaints: Taint[] = [];
|
||||
const taints = node.spec?.taints || [];
|
||||
taints.forEach((taint) => {
|
||||
const matchKeyMatchValueMatchEffect = _.find(tolerations, {
|
||||
key: taint.key,
|
||||
operator: 'Equal',
|
||||
value: taint.value,
|
||||
effect: taint.effect,
|
||||
});
|
||||
const matchKeyAnyValueMatchEffect = _.find(tolerations, {
|
||||
key: taint.key,
|
||||
operator: 'Exists',
|
||||
effect: taint.effect,
|
||||
});
|
||||
const matchKeyMatchValueAnyEffect = _.find(tolerations, {
|
||||
key: taint.key,
|
||||
operator: 'Equal',
|
||||
value: taint.value,
|
||||
effect: '',
|
||||
});
|
||||
const matchKeyAnyValueAnyEffect = _.find(tolerations, {
|
||||
key: taint.key,
|
||||
operator: 'Exists',
|
||||
effect: '',
|
||||
});
|
||||
const anyKeyAnyValueAnyEffect = _.find(tolerations, {
|
||||
key: '',
|
||||
operator: 'Exists',
|
||||
effect: '',
|
||||
});
|
||||
if (
|
||||
!matchKeyMatchValueMatchEffect &&
|
||||
!matchKeyAnyValueMatchEffect &&
|
||||
!matchKeyMatchValueAnyEffect &&
|
||||
!matchKeyAnyValueAnyEffect &&
|
||||
!anyKeyAnyValueAnyEffect
|
||||
) {
|
||||
acceptsApplication = false;
|
||||
unmetTaints?.push(taint);
|
||||
} else {
|
||||
acceptsApplication = true;
|
||||
}
|
||||
});
|
||||
return {
|
||||
name: node.metadata?.name || '',
|
||||
acceptsApplication,
|
||||
unmetTaints,
|
||||
highlighted: false,
|
||||
};
|
||||
});
|
||||
|
||||
return nodePlacements;
|
||||
}
|
||||
|
||||
// Node requirement depending on the operator value
|
||||
// https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity
|
||||
function computeAffinities(
|
||||
nodes: Node[],
|
||||
nodePlacements: NodePlacementRowData[],
|
||||
pod: Pod
|
||||
): NodePlacementRowData[] {
|
||||
const nodePlacementsFromAffinities: NodePlacementRowData[] = nodes.map(
|
||||
(node, nodeIndex) => {
|
||||
let { acceptsApplication } = nodePlacements[nodeIndex];
|
||||
|
||||
if (pod.spec?.nodeSelector) {
|
||||
const patch = JsonPatch.compare(
|
||||
node.metadata?.labels || {},
|
||||
pod.spec.nodeSelector
|
||||
);
|
||||
_.remove(patch, { op: 'remove' });
|
||||
const unmatchedNodeSelectorLabels = patch.map((operation) => ({
|
||||
key: _.trimStart(operation.path, '/'),
|
||||
value: operation.op,
|
||||
}));
|
||||
if (unmatchedNodeSelectorLabels.length) {
|
||||
acceptsApplication = false;
|
||||
}
|
||||
}
|
||||
|
||||
const basicNodeAffinity =
|
||||
pod.spec?.affinity?.nodeAffinity
|
||||
?.requiredDuringSchedulingIgnoredDuringExecution;
|
||||
if (basicNodeAffinity) {
|
||||
const unmatchedTerms = basicNodeAffinity.nodeSelectorTerms.map(
|
||||
(selectorTerm) => {
|
||||
const unmatchedExpressions = selectorTerm.matchExpressions?.flatMap(
|
||||
(matchExpression) => {
|
||||
const exists = {}.hasOwnProperty.call(
|
||||
node.metadata?.labels,
|
||||
matchExpression.key
|
||||
);
|
||||
const isIn =
|
||||
exists &&
|
||||
_.includes(
|
||||
matchExpression.values,
|
||||
node.metadata?.labels?.[matchExpression.key]
|
||||
);
|
||||
if (
|
||||
(matchExpression.operator === 'Exists' && exists) ||
|
||||
(matchExpression.operator === 'DoesNotExist' && !exists) ||
|
||||
(matchExpression.operator === 'In' && isIn) ||
|
||||
(matchExpression.operator === 'NotIn' && !isIn) ||
|
||||
(matchExpression.operator === 'Gt' &&
|
||||
exists &&
|
||||
parseInt(
|
||||
node.metadata?.labels?.[matchExpression.key] || '',
|
||||
10
|
||||
) > parseInt(matchExpression.values?.[0] || '', 10)) ||
|
||||
(matchExpression.operator === 'Lt' &&
|
||||
exists &&
|
||||
parseInt(
|
||||
node.metadata?.labels?.[matchExpression.key] || '',
|
||||
10
|
||||
) < parseInt(matchExpression.values?.[0] || '', 10))
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return [true];
|
||||
}
|
||||
);
|
||||
|
||||
return unmatchedExpressions;
|
||||
}
|
||||
);
|
||||
|
||||
_.remove(unmatchedTerms, (i) => i?.length === 0);
|
||||
if (unmatchedTerms.length) {
|
||||
acceptsApplication = false;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...nodePlacements[nodeIndex],
|
||||
acceptsApplication,
|
||||
};
|
||||
}
|
||||
);
|
||||
return nodePlacementsFromAffinities;
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
|
||||
import {
|
||||
useApplication,
|
||||
useApplicationPods,
|
||||
useApplicationServices,
|
||||
} from '../application.queries';
|
||||
|
||||
import { useNamespaceEventsQuery } from './useNamespaceEventsQuery';
|
||||
|
||||
const storageKey = 'k8sAppEventsDatatable';
|
||||
const settingsStore = createStore(storageKey, { id: 'Date', desc: true });
|
||||
|
||||
export function useApplicationEventsTableState() {
|
||||
return useTableState(settingsStore, storageKey);
|
||||
}
|
||||
|
||||
export function useApplicationEventsTableData() {
|
||||
const appEventsTableState = useTableState(settingsStore, storageKey);
|
||||
const {
|
||||
params: {
|
||||
namespace,
|
||||
name,
|
||||
'resource-type': resourceType,
|
||||
endpointId: environmentId,
|
||||
},
|
||||
} = useCurrentStateAndParams();
|
||||
|
||||
const { data: application, ...applicationQuery } = useApplication(
|
||||
environmentId,
|
||||
namespace,
|
||||
name,
|
||||
resourceType
|
||||
);
|
||||
const { data: services, ...servicesQuery } = useApplicationServices(
|
||||
environmentId,
|
||||
namespace,
|
||||
name,
|
||||
application
|
||||
);
|
||||
const { data: pods, ...podsQuery } = useApplicationPods(
|
||||
environmentId,
|
||||
namespace,
|
||||
name,
|
||||
application
|
||||
);
|
||||
const { data: events, ...eventsQuery } = useNamespaceEventsQuery(
|
||||
environmentId,
|
||||
namespace,
|
||||
{
|
||||
autoRefreshRate: appEventsTableState.autoRefreshRate * 1000,
|
||||
}
|
||||
);
|
||||
|
||||
// related events are events that have the application id, or the id of a service or pod from the application
|
||||
const appEventsData = useMemo(() => {
|
||||
const serviceIds = services?.map((service) => service?.metadata?.uid);
|
||||
const podIds = pods?.map((pod) => pod?.metadata?.uid);
|
||||
return (
|
||||
events?.filter(
|
||||
(event) =>
|
||||
event.involvedObject.uid === application?.metadata?.uid ||
|
||||
serviceIds?.includes(event.involvedObject.uid) ||
|
||||
podIds?.includes(event.involvedObject.uid)
|
||||
) || []
|
||||
);
|
||||
}, [application?.metadata?.uid, events, pods, services]);
|
||||
|
||||
const appEventWarningCount = useMemo(
|
||||
() => appEventsData.filter((event) => event.type === 'Warning').length,
|
||||
[appEventsData]
|
||||
);
|
||||
|
||||
return {
|
||||
appEventsData,
|
||||
appEventWarningCount,
|
||||
isAppEventsTableLoading:
|
||||
applicationQuery.isLoading ||
|
||||
servicesQuery.isLoading ||
|
||||
podsQuery.isLoading ||
|
||||
eventsQuery.isLoading,
|
||||
};
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { Node } from '../../types';
|
||||
|
||||
export const columnHelper = createColumnHelper<Node>();
|
@ -1 +0,0 @@
|
||||
export { PlacementsDatatable } from './PlacementsDatatable';
|
@ -1,14 +1,88 @@
|
||||
import { HorizontalPodAutoscalerList } from 'kubernetes-types/autoscaling/v1';
|
||||
import {
|
||||
HorizontalPodAutoscaler,
|
||||
HorizontalPodAutoscalerList,
|
||||
} from 'kubernetes-types/autoscaling/v1';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import axios from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
|
||||
import { parseKubernetesAxiosError } from '../axiosError';
|
||||
|
||||
// when yaml is set to true, the expected return type is a string
|
||||
export function useHorizontalAutoScalarQuery<
|
||||
T extends HorizontalPodAutoscaler | string = HorizontalPodAutoscaler
|
||||
>(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
name?: string,
|
||||
options?: { yaml?: boolean }
|
||||
) {
|
||||
return useQuery(
|
||||
[
|
||||
'environments',
|
||||
environmentId,
|
||||
'kubernetes',
|
||||
'namespaces',
|
||||
namespace,
|
||||
'horizontalpodautoscalers',
|
||||
name,
|
||||
options?.yaml,
|
||||
],
|
||||
() =>
|
||||
name
|
||||
? getNamespaceHorizontalPodAutoscaler<T>(
|
||||
environmentId,
|
||||
namespace,
|
||||
name,
|
||||
options
|
||||
)
|
||||
: undefined,
|
||||
{ ...withError('Unable to get horizontal pod autoscaler'), enabled: !!name }
|
||||
);
|
||||
}
|
||||
|
||||
export async function getNamespaceHorizontalPodAutoscalers(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string
|
||||
) {
|
||||
const { data: autoScalarList } = await axios.get<HorizontalPodAutoscalerList>(
|
||||
`/endpoints/${environmentId}/kubernetes/apis/autoscaling/v1/namespaces/${namespace}/horizontalpodautoscalers`
|
||||
);
|
||||
return autoScalarList.items;
|
||||
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 as Error,
|
||||
'Unable to retrieve horizontal pod autoscalers'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNamespaceHorizontalPodAutoscaler<
|
||||
T extends HorizontalPodAutoscaler | string = HorizontalPodAutoscaler
|
||||
>(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
name: string,
|
||||
options?: { yaml?: boolean }
|
||||
) {
|
||||
try {
|
||||
const { data: autoScalar } = await axios.get<T>(
|
||||
`/endpoints/${environmentId}/kubernetes/apis/autoscaling/v1/namespaces/${namespace}/horizontalpodautoscalers/${name}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: options?.yaml ? 'application/yaml' : 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
return autoScalar;
|
||||
} catch (e) {
|
||||
throw parseKubernetesAxiosError(
|
||||
e as Error,
|
||||
'Unable to retrieve horizontal pod autoscaler'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,70 @@
|
||||
import { NodeList, Node } from 'kubernetes-types/core/v1';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import axios from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
|
||||
const queryKeys = {
|
||||
node: (environmentId: number, nodeName: string) => [
|
||||
'environments',
|
||||
environmentId,
|
||||
'kubernetes',
|
||||
'nodes',
|
||||
nodeName,
|
||||
],
|
||||
nodes: (environmentId: number) => [
|
||||
'environments',
|
||||
environmentId,
|
||||
'kubernetes',
|
||||
'nodes',
|
||||
],
|
||||
};
|
||||
|
||||
async function getNode(environmentId: EnvironmentId, nodeName: string) {
|
||||
const { data: node } = await axios.get<Node>(
|
||||
`/endpoints/${environmentId}/kubernetes/api/v1/nodes/${nodeName}`
|
||||
);
|
||||
return node;
|
||||
}
|
||||
|
||||
export function useNodeQuery(environmentId: EnvironmentId, nodeName: string) {
|
||||
return useQuery(
|
||||
queryKeys.node(environmentId, nodeName),
|
||||
() => getNode(environmentId, nodeName),
|
||||
{
|
||||
...withError(
|
||||
'Unable to get node details from the Kubernetes api',
|
||||
'Failed to get node details'
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// getNodes is used to get a list of nodes using the kubernetes API
|
||||
async function getNodes(environmentId: EnvironmentId) {
|
||||
const { data: nodeList } = await axios.get<NodeList>(
|
||||
`/endpoints/${environmentId}/kubernetes/api/v1/nodes`
|
||||
);
|
||||
return nodeList.items;
|
||||
}
|
||||
|
||||
// useNodesQuery is used to get an array of nodes using the kubernetes API
|
||||
export function useNodesQuery(
|
||||
environmentId: EnvironmentId,
|
||||
options?: { autoRefreshRate?: number }
|
||||
) {
|
||||
return useQuery(
|
||||
queryKeys.nodes(environmentId),
|
||||
async () => getNodes(environmentId),
|
||||
{
|
||||
...withError(
|
||||
'Failed to get nodes from the Kubernetes api',
|
||||
'Failed to get nodes'
|
||||
),
|
||||
refetchInterval() {
|
||||
return options?.autoRefreshRate ?? false;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Reference in new issue