1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

refactor(app): app service form to react [EE-5415] (#8994)

This commit is contained in:
Ali 2023-05-31 17:58:41 +12:00 committed by GitHub
parent 2d05103fed
commit 69776b4863
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1224 additions and 538 deletions

View file

@ -1,95 +0,0 @@
import _ from 'lodash-es';
import { KubernetesServicePort } from 'Kubernetes/models/service/models';
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models/constants';
export default class KubeServicesItemViewController {
/* @ngInject */
constructor(EndpointProvider, Authentication) {
this.EndpointProvider = EndpointProvider;
this.Authentication = Authentication;
this.KubernetesApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
}
addPort() {
const port = new KubernetesServicePort();
port.nodePort = '';
port.port = '';
port.targetPort = '';
port.protocol = 'TCP';
this.service.Ports.push(port);
}
removePort(index) {
this.service.Ports.splice(index, 1);
}
servicePort(index) {
const targetPort = this.service.Ports[index].targetPort;
this.service.Ports[index].port = targetPort;
this.onChangeServicePort();
}
isAdmin() {
return this.Authentication.isAdmin();
}
onChangeContainerPort() {
const state = this.state.duplicates.targetPort;
const source = _.map(this.service.Ports, (sp) => sp.targetPort);
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasRefs = Object.keys(duplicates).length > 0;
}
onChangeServicePort() {
const state = this.state.duplicates.servicePort;
const source = _.map(this.service.Ports, (sp) => sp.port);
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasRefs = Object.keys(duplicates).length > 0;
this.service.servicePortError = state.hasRefs;
}
onChangeNodePort() {
const state = this.state.duplicates.nodePort;
// create a list of all the node ports (number[]) in the cluster and current form
const clusterNodePortsWithoutCurrentService = this.nodePortServices
.filter((npService) => npService.Name !== this.service.Name)
.map((npService) => npService.Ports)
.flat()
.map((npServicePorts) => npServicePorts.NodePort);
const formNodePortsWithoutCurrentService = this.formServices
.filter((formService) => formService.Type === KubernetesApplicationPublishingTypes.NODE_PORT && formService.Name !== this.service.Name)
.map((formService) => formService.Ports)
.flat()
.map((formServicePorts) => formServicePorts.nodePort);
const serviceNodePorts = this.service.Ports.map((sp) => sp.nodePort);
// getDuplicates cares about the index, so put the serviceNodePorts at the start
const allNodePortsWithoutCurrentService = [...clusterNodePortsWithoutCurrentService, ...formNodePortsWithoutCurrentService];
const duplicates = KubernetesFormValidationHelper.getDuplicateNodePorts(serviceNodePorts, allNodePortsWithoutCurrentService);
state.refs = duplicates;
state.hasRefs = Object.keys(duplicates).length > 0;
this.service.nodePortError = state.hasRefs;
}
$onInit() {
if (this.service.Ports.length === 0) {
this.addPort();
}
this.state = {
duplicates: {
targetPort: new KubernetesFormValidationReferences(),
servicePort: new KubernetesFormValidationReferences(),
nodePort: new KubernetesFormValidationReferences(),
},
endpointId: this.EndpointProvider.endpointID(),
};
}
}

View file

@ -1,188 +0,0 @@
<ng-form name="serviceForm">
<div ng-if="$ctrl.isAdmin()" class="small" ng-show="$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled">
<p class="text-warning vertical-center pt-2">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> No Load balancer is available in this cluster, click
<a class="hyperlink" ui-sref="kubernetes.cluster.setup">here</a> to configure load balancer.
</p>
</div>
<div ng-if="!$ctrl.isAdmin()" class="small" ng-show="$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled">
<p class="text-warning vertical-center pt-2">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> No Load balancer is available in this cluster, contact your administrator.
</p>
</div>
<div
ng-if="
($ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && $ctrl.loadbalancerEnabled) ||
$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP ||
$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT
"
>
<div ng-show="!$ctrl.multiItemDisable" class="vertical-center mt-5 mb-5">
<label class="control-label !pt-0 text-left">Published ports</label>
</div>
<div ng-repeat="servicePort in $ctrl.service.Ports" class="service-form row mt-5">
<div class="form-group col-sm-3 !mx-0 !pl-0">
<div class="input-group input-group-sm">
<span class="input-group-addon required">Container port</span>
<input
type="number"
class="form-control"
name="container_port_{{ $index }}"
ng-model="servicePort.targetPort"
placeholder="80"
ng-min="1"
ng-max="65535"
min="1"
max="65535"
ng-change="$ctrl.servicePort($index)"
required
ng-disabled="$ctrl.originalIngresses.length === 0 || ($ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled)"
ng-change="$ctrl.onChangeContainerPort()"
data-cy="k8sAppCreate-containerPort_{{ $index }}"
/>
</div>
<span>
<div class="small text-warning mt-1" ng-if="$ctrl.state.duplicates.targetPort.refs[$index] !== undefined">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This container port is already used.
</div>
<div class="small text-warning mt-1" ng-messages="serviceForm['container_port_'+$index].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Container port number is required.</p>
<p class="vertical-center" ng-message="min"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Container port number must be inside the range 1-65535.</p>
<p class="vertical-center" ng-message="max"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Container port number must be inside the range 1-65535.</p>
</div>
</span>
</div>
<div class="form-group col-sm-3 !mx-0 !pl-0">
<div class="input-group input-group-sm">
<span class="input-group-addon required">Service port</span>
<input
type="number"
class="form-control"
name="service_port_{{ $index }}"
ng-model="servicePort.port"
placeholder="80"
ng-min="1"
ng-max="65535"
min="1"
max="65535"
required
ng-disabled="$ctrl.originalIngresses.length === 0 || ($ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled)"
ng-change="$ctrl.onChangeServicePort()"
data-cy="k8sAppCreate-servicePort_{{ $index }}"
/>
</div>
<span>
<div class="small text-warning mt-1" ng-if="$ctrl.state.duplicates.servicePort.refs[$index] !== undefined">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This service port is already used.
</div>
<div class="small text-warning mt-1">
<div ng-messages="serviceForm['service_port_'+$index].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Service port number is required.</p>
<p class="vertical-center" ng-message="min"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Service port number must be inside the range 1-65535.</p>
<p class="vertical-center" ng-message="max"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Service port number must be inside the range 1-65535.</p>
</div>
</div>
</span>
</div>
<div class="form-group col-sm-3 !mx-0 !pl-0" ng-if="$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT">
<div class="input-group input-group-sm">
<span class="input-group-addon required">Nodeport</span>
<input
type="number"
class="form-control"
name="node_port_{{ $index }}"
ng-model="servicePort.nodePort"
placeholder="30080"
ng-min="30000"
ng-max="32767"
min="30000"
max="32767"
required
ng-change="$ctrl.onChangeNodePort()"
data-cy="k8sAppCreate-nodeportPort_{{ $index }}"
/>
</div>
<div class="w-full">
<span>
<div class="small text-warning mt-1">
<div ng-messages="serviceForm['node_port_'+$index].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Nodeport is required.</p>
<p class="vertical-center" ng-message="min"
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Nodeport number must be inside the range 30000-32767 or blank for system allocated.</p
>
<p class="vertical-center" ng-message="max"
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Nodeport number must be inside the range 30000-32767 or blank for system allocated.</p
>
<div class="text-warning mt-1" ng-if="$ctrl.state.duplicates.nodePort.refs[$index] !== undefined">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This node port is already used.
</div>
</div>
</div>
</span>
</div>
</div>
<div class="form-group col-sm-3 !mx-0 !pl-0" ng-if="$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER">
<div class="input-group input-group-sm">
<span class="input-group-addon">Loadbalancer port</span>
<input
type="number"
class="form-control"
name="loadbalancer_port_{{ $index }}"
ng-model="servicePort.port"
placeholder="80"
ng-min="1"
ng-max="65535"
min="1"
max="65535"
required
ng-disabled="$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled"
data-cy="k8sAppCreate-loadbalancerPort_{{ $index }}"
/>
</div>
</div>
<div class="form-group col-sm-3 !mx-0 !pl-0">
<div class="input-group input-group-sm">
<div class="btn-group btn-group-sm">
<label
class="btn btn-light"
ng-model="servicePort.protocol"
uib-btn-radio="'TCP'"
ng-change="ctrl.onChangePortProtocol($index)"
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'TCP')"
data-cy="k8sAppCreate-TCPButton_{{ $index }}"
>TCP</label
>
<label
class="btn btn-light"
ng-model="servicePort.protocol"
uib-btn-radio="'UDP'"
ng-change="ctrl.onChangePortProtocol($index)"
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'UDP')"
data-cy="k8sAppCreate-UDPButton_{{ $index }}"
>UDP</label
>
</div>
<button
ng-disabled="$ctrl.service.Ports.length === 1"
ng-show="!$ctrl.multiItemDisable"
class="btn btn-sm btn-dangerlight btn-only-icon"
type="button"
ng-click="$ctrl.removePort($index)"
data-cy="k8sAppCreate-rmPortButton_{{ $index }}"
>
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
</div>
</div>
</div>
<div class="mt-4">
<span class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0" ng-click="$ctrl.addPort()" data-cy="k8sAppCreate-addNewPortButton">
<pr-icon icon="'plus'" size="'sm'"></pr-icon> Publish a new port
</span>
</div>
</div>
</ng-form>

View file

@ -1,14 +0,0 @@
import angular from 'angular';
import controller from './kube-services-item.controller';
angular.module('portainer.kubernetes').component('kubeServicesItemView', {
templateUrl: './kube-services-item.html',
controller,
bindings: {
nodePortServices: '<',
formServices: '<',
service: '=',
isEdit: '<',
loadbalancerEnabled: '<',
},
});

View file

@ -1,105 +0,0 @@
import { KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from '@/kubernetes/models/service/models';
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models/constants';
import { notifyError } from '@/portainer/services/notifications';
import { getServices } from '@/react/kubernetes/networks/services/service';
export default class KubeServicesViewController {
/* @ngInject */
constructor($async, EndpointProvider, Authentication) {
this.$async = $async;
this.EndpointProvider = EndpointProvider;
this.Authentication = Authentication;
this.asyncOnInit = this.asyncOnInit.bind(this);
}
addEntry(service) {
const p = new KubernetesService();
p.Type = service;
p.Selector = this.formValues.Selector;
p.Name = this.getUniqName();
this.formValues.Services.push(p);
}
getUniqName() {
//services name will follow thia patten: service, service-2, service-3...
let nameIndex = 2;
let UniqName = this.formValues.Name;
const services = this.formValues.Services;
const sortServices = services.sort((a, b) => {
return a.Name.localeCompare(b.Name);
});
if (sortServices.length !== 0) {
sortServices.forEach((service) => {
if (service.Name === UniqName) {
UniqName = this.formValues.Name + '-' + nameIndex;
nameIndex += 1;
}
});
}
return UniqName;
}
deleteService(index) {
this.formValues.Services.splice(index, 1);
}
addPort(index) {
const p = new KubernetesServicePort();
this.formValues.Services[index].Ports.push(p);
}
serviceType(type) {
switch (type) {
case KubernetesApplicationPublishingTypes.CLUSTER_IP:
return KubernetesServiceTypes.CLUSTER_IP;
case KubernetesApplicationPublishingTypes.NODE_PORT:
return KubernetesServiceTypes.NODE_PORT;
case KubernetesApplicationPublishingTypes.LOAD_BALANCER:
return KubernetesServiceTypes.LOAD_BALANCER;
}
}
isAdmin() {
return this.Authentication.isAdmin();
}
async asyncOnInit() {
try {
// get all nodeport services in the cluster, to validate unique nodeports in the form
const allSettledServices = await Promise.allSettled(this.namespaces.map((namespace) => getServices(this.state.endpointId, namespace)));
const allServices = allSettledServices
.filter((settledService) => settledService.status === 'fulfilled' && settledService.value)
.map((fulfilledService) => fulfilledService.value)
.flat();
this.nodePortServices = allServices.filter((service) => service.Type === 'NodePort');
} catch (error) {
notifyError('Failure', error, 'Failed getting services');
}
}
$onInit() {
this.state = {
serviceType: [
{
typeName: KubernetesServiceTypes.CLUSTER_IP,
typeValue: KubernetesApplicationPublishingTypes.CLUSTER_IP,
},
{
typeName: KubernetesServiceTypes.NODE_PORT,
typeValue: KubernetesApplicationPublishingTypes.NODE_PORT,
},
{
typeName: KubernetesServiceTypes.LOAD_BALANCER,
typeValue: KubernetesApplicationPublishingTypes.LOAD_BALANCER,
},
],
selected: KubernetesApplicationPublishingTypes.CLUSTER_IP,
endpointId: this.EndpointProvider.endpointID(),
};
return this.$async(this.asyncOnInit);
}
}

View file

@ -1,91 +0,0 @@
<div class="col-sm-12 form-section-title"> Publishing the application </div>
<div class="col-sm-12 !p-0">
<div class="small">
<p class="text-muted vertical-center">
<pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon>
<span>Publish your application by creating a ClusterIP service for it, which you may then expose via <a target="_blank" ui-sref="kubernetes.ingresses">an ingress</a>.</span>
</p>
</div>
</div>
<div class="form-group">
<div class="col-sm-12 form-inline">
<div class="col-sm-6" style="padding-left: 0px">
<select
class="form-control"
ng-model="$ctrl.state.selected"
ng-options="item.typeValue as item.typeName for item in $ctrl.state.serviceType"
data-cy="k8sAppCreate-publishingModeDropdown"
></select>
<button type="button" class="btn btn-md btn-default vertical-center !ml-0" ng-click="$ctrl.addEntry( $ctrl.state.selected )" data-cy="k8sAppCreate-createServiceButton">
<span
class="interactive vertical-center"
tooltip-append-to-body="true"
tooltip-placement="top"
tooltip-class="portainer-tooltip"
uib-tooltip="Different service types expose the deployment in alternate ways.
ClusterIP exposes it within the cluster (for internal access only).
NodePort exposes it (on a high port) across all nodes.
LoadBalancer exposes it via an external load balancer."
>
<pr-icon icon="'plus'" size="'sm'"></pr-icon> Create service
</span>
</button>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-12 form-inline" style="margin-top: 20px" ng-repeat="service in $ctrl.formValues.Services">
<div>
<div class="text-muted vertical-center">
<pr-icon ng-if="$ctrl.serviceType(service.Type) === 'ClusterIP'" icon="'list'"></pr-icon>
<pr-icon ng-if="$ctrl.serviceType(service.Type) === 'LoadBalancer'" icon="'svg-dataflow'"></pr-icon>
<pr-icon ng-if="$ctrl.serviceType(service.Type) === 'NodePort'" icon="'list'"></pr-icon>
{{ $ctrl.serviceType(service.Type) }}
</div>
<kube-services-item-view
node-port-services="$ctrl.nodePortServices"
form-services="$ctrl.formValues.Services"
service="$ctrl.formValues.Services[$index]"
is-edit="$ctrl.isEdit"
loadbalancer-enabled="$ctrl.loadbalancerEnabled"
></kube-services-item-view>
<button
type="button"
class="btn btn-sm btn-dangerlight space-right vertical-center"
style="margin-left: 0; margin-top: 10px"
ng-click="$ctrl.deleteService( $index )"
data-cy="k8sConfigCreate-removeButton"
>
<pr-icon icon="'trash-2'" size="'md'"></pr-icon> Remove
</button>
</div>
<div ng-if="$ctrl.formValues.Services[$index].Ingress && $ctrl.formValues.OriginalIngresses.length === 0">
<div class="text-muted">
<pr-icon icon="'svg-route'" class-name="'mr-0.5'"></pr-icon>
Ingress
</div>
<div ng-if="$ctrl.isAdmin()" class="small">
<p class="text-warning vertical-center pt-2">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Ingress is not configured in this namespace, select another namespace or click
<a ui-sref="kubernetes.cluster.setup">here</a> to configure ingress.
</p>
</div>
<div ng-if="!$ctrl.isAdmin()" class="small">
<p class="text-warning vertical-center pt-2">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Ingress is not configured in this namespace, select another namespace or contact your administrator.
</p>
</div>
<button
type="button"
class="btn btn-sm btn-dangerlight space-right vertical-center"
style="margin-left: 0; margin-top: 10px"
ng-click="$ctrl.deleteService( $index )"
data-cy="k8sConfigCreate-removeButton"
>
<pr-icon icon="'trash-2'" size="'md'"></pr-icon> Remove
</button>
</div>
</div>
</div>

View file

@ -1,13 +0,0 @@
import angular from 'angular';
import controller from './kube-services.controller';
angular.module('portainer.kubernetes').component('kubeServicesView', {
templateUrl: './kube-services.html',
controller,
bindings: {
formValues: '=',
isEdit: '<',
namespaces: '<',
loadbalancerEnabled: '<',
},
});