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

refactor(app): details widget migration [EE-5352] (#8886)

This commit is contained in:
Ali 2023-05-29 15:06:14 +12:00 committed by GitHub
parent fdd79cece8
commit af77e33993
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 2046 additions and 1079 deletions

View file

@ -86,308 +86,7 @@
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<div ng-if="!ctrl.isSystemNamespace()" class="mb-4 flex items-center gap-1">
<button
ng-if="!ctrl.isExternalApplication()"
type="button"
class="btn btn-sm btn-light vertical-center ml-2"
ui-sref="kubernetes.applications.application.edit"
style="margin-left: 0"
data-cy="k8sAppDetail-editAppButton"
>
<pr-icon icon="'pencil'" class="mr-1"></pr-icon>{{ ctrl.stack.IsComposeFormat ? 'View this application' : 'Edit this application' }}
</button>
<button
authorization="K8sApplicationDetailsW"
ng-if="ctrl.isExternalApplication()"
type="button"
class="btn btn-sm btn-light ml-2"
ui-sref="kubernetes.applications.application.edit"
data-cy="k8sAppDetail-editAppButton"
>
<pr-icon icon="'pencil'" class-name="'mr-1'"></pr-icon>Edit external application
</button>
<be-teaser-button
icon="'refresh-cw'"
feature-id="ctrl.limitedFeature"
message="'A rolling restart of the application is performed.'"
heading="'Rolling restart'"
button-text="'Rolling restart'"
class-name="'be-tooltip-teaser'"
className="'be-tooltip-teaser'"
></be-teaser-button>
<button
ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD"
type="button"
class="btn btn-sm btn-light ml-2"
ng-click="ctrl.redeployApplication()"
data-cy="k8sAppDetail-redeployButton"
>
<pr-icon icon="'rotate-cw'" class="'mr-1'"></pr-icon>Redeploy
</button>
<button
ng-if="!ctrl.isExternalApplication()"
type="button"
class="btn btn-sm btn-light"
ng-click="ctrl.rollbackApplication()"
ng-disabled="ctrl.application.Revisions.length < 2 || ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
data-cy="k8sAppDetail-rollbackButton"
>
<pr-icon icon="'rotate-ccw'" class="mr-1"></pr-icon>Rollback to previous configuration
</button>
<a
ng-if="ctrl.isStack() && ctrl.stackFileContent"
class="btn btn-sm btn-primary space-left"
ui-sref="kubernetes.templates.custom.new({fileContent: ctrl.stackFileContent})"
>
<pr-icon icon="'plus'" class="mr-1"></pr-icon>Create template from application
</a>
</div>
<!-- ACCESSING APPLICATION -->
<div class="text-muted" style="margin-bottom: 15px"> <pr-icon icon="'external-link'" class="mr-1"></pr-icon>Accessing the application </div>
<div class="small text-muted" ng-if="ctrl.application.PublishedPorts.length === 0" style="margin-bottom: 15px">
<pr-icon icon="'info'" mode="'primary'" class="mr-1"></pr-icon>This application is not exposing any port.
</div>
<div ng-if="ctrl.application.Services.length !== 0">
<!-- Services notice -->
<div>
<div class="small text-muted">
<p> <pr-icon icon="'info'" mode="'primary'" class="mr-1"></pr-icon>This application is exposed through service(s) as below: </p>
</div>
</div>
<!-- table -->
<kubernetes-application-services-table
services="ctrl.application.Services"
application="ctrl.application"
public-url="ctrl.state.publicUrl"
></kubernetes-application-services-table>
<!-- table -->
<!-- table -->
<kubernetes-application-ingress-table application="ctrl.application" public-url="ctrl.state.publicUrl"></kubernetes-application-ingress-table>
<!-- table -->
</div>
<!-- !ACCESSING APPLICATION -->
<!-- AUTO SCALING -->
<div class="text-muted" style="margin-bottom: 15px"> <pr-icon icon="'move'" class="mr-1"></pr-icon>Auto-scaling </div>
<div class="small text-muted" ng-if="!ctrl.application.AutoScaler" style="margin-bottom: 15px">
<pr-icon icon="'info'" mode="'primary'" class="mr-1"></pr-icon>
This application does not have an autoscaling policy defined.
</div>
<div ng-if="ctrl.application.AutoScaler">
<div style="margin-top: 15px; width: 50%">
<table class="table">
<tbody>
<tr class="text-muted">
<td style="width: 33%">Minimum instances</td>
<td style="width: 33%">Maximum instances</td>
<td style="width: 33%">
Target CPU usage
<portainer-tooltip message="'The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances.'">
</portainer-tooltip>
</td>
</tr>
<tr>
<td data-cy="k8sAppDetail-minReplicas">{{ ctrl.application.AutoScaler.MinReplicas }}</td>
<td data-cy="k8sAppDetail-maxReplicas">{{ ctrl.application.AutoScaler.MaxReplicas }}</td>
<td data-cy="k8sAppDetail-targetCPU">{{ ctrl.application.AutoScaler.TargetCPUUtilization }}%</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- !AUTO SCALING -->
<!-- CONFIGURATIONS -->
<div class="text-muted" style="margin-bottom: 15px; margin-top: 25px">
<pr-icon icon="'file'" class="mr-1"></pr-icon>
ConfigMap or Secret
</div>
<div class="small text-muted" ng-if="!ctrl.application.Env.length > 0 && !ctrl.hasVolumeConfiguration()" style="margin-bottom: 15px">
<pr-icon icon="'info'" mode="'primary'" class="mr-1"></pr-icon>
This application is not using any environment variable, ConfigMap or Secret.
</div>
<table class="table" ng-if="ctrl.application.Env.length > 0">
<tr class="text-muted">
<td style="width: 25%">Container</td>
<td style="width: 25%">Environment variable</td>
<td style="width: 25%">Value</td>
<td style="width: 25%">ConfigMap or Secret</td>
</tr>
<tbody ng-repeat="container in ctrl.application.Containers" style="border-top: 0">
<tr ng-repeat="envvar in container.Env | orderBy: 'name'">
<td data-cy="k8sAppDetail-containerName">
{{ container.Name }}
<span ng-if="container.Type === ctrl.KubernetesPodContainerTypes.INIT">
<pr-icon icon="'asterisk'"></pr-icon>
{{ envvar.valueFrom.fieldRef.fieldPath }} (<a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" target="_blank">init container</a
>)</span
>
</td>
<td data-cy="k8sAppDetail-envVarName">{{ envvar.name }}</td>
<td>
<span ng-if="envvar.value" data-cy="k8sAppDetail-envVarValue">{{ envvar.value }}</span>
<span ng-if="envvar.valueFrom.configMapKeyRef" data-cy="k8sAppDetail-envVarValue"
><pr-icon icon="'key'" class="mr-1"></pr-icon>{{ envvar.valueFrom.configMapKeyRef.key }}</span
>
<span ng-if="envvar.valueFrom.secretKeyRef" data-cy="k8sAppDetail-envVarValue"
><pr-icon icon="'key'" class="mr-1"></pr-icon>{{ envvar.valueFrom.secretKeyRef.key }}</span
>
<span ng-if="envvar.valueFrom.fieldRef" data-cy="k8sAppDetail-envVarValue"
><pr-icon icon="'asterisk'"></pr-icon> {{ envvar.valueFrom.fieldRef.fieldPath }} (<a
href="https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/#capabilities-of-the-downward-api"
target="_blank"
>downward API</a
>)</span
>
<span ng-if="!envvar.value && !envvar.valueFrom.secretKeyRef && !envvar.valueFrom.configMapKeyRef && !envvar.valueFrom.fieldRef">-</span>
</td>
<td>
<span ng-if="envvar.value || envvar.valueFrom.fieldRef || (!envvar.valueFrom.secretKeyRef && !envvar.valueFrom.configMapKeyRef)">-</span>
<span ng-if="envvar.valueFrom.configMapKeyRef" data-cy="k8sAppDetail-configName"
><a ui-sref="kubernetes.configurations.configuration({ name: envvar.valueFrom.configMapKeyRef.name, namespace: ctrl.application.ResourcePool })"
><pr-icon icon="'file'" class="mr-1"></pr-icon>{{ envvar.valueFrom.configMapKeyRef.name }}</a
></span
>
<span ng-if="envvar.valueFrom.secretKeyRef" data-cy="k8sAppDetail-configName"
><a ui-sref="kubernetes.configurations.configuration({ name: envvar.valueFrom.secretKeyRef.name, namespace: ctrl.application.ResourcePool })"
><pr-icon icon="'file'" class="mr-1"></pr-icon>{{ envvar.valueFrom.secretKeyRef.name }}</a
></span
>
</td>
</tr>
</tbody>
</table>
<table class="table" ng-if="ctrl.hasVolumeConfiguration()">
<tr class="text-muted">
<td style="width: 25%">Container</td>
<td style="width: 25%">Configuration path</td>
<td style="width: 25%">Value</td>
<td style="width: 25%">Configuration</td>
</tr>
<tbody ng-repeat="container in ctrl.application.Containers" style="border-top: 0">
<tr ng-repeat="volume in container.ConfigurationVolumes track by $index" style="border-top: 0">
<td>
{{ container.Name }}
<span ng-if="container.Type === ctrl.KubernetesPodContainerTypes.INIT"
><pr-icon icon="'asterisk'"></pr-icon> {{ envvar.valueFrom.fieldRef.fieldPath }} (<a
href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/"
target="_blank"
>init container</a
>)</span
>
</td>
<td>
{{ volume.fileMountPath }}
</td>
<td>
<pr-icon icon="'plus'" class="mr-1" ng-if="volume.configurationKey"></pr-icon>
{{ volume.configurationKey ? volume.configurationKey : '-' }}
</td>
<td>
<a ui-sref="kubernetes.configurations.configuration({ name: volume.configurationName, namespace: ctrl.application.ResourcePool })"
><pr-icon icon="'plus'" class="mr-1"></pr-icon>{{ volume.configurationName }}</a
>
</td>
</tr>
</tbody>
</table>
<!-- !CONFIGURATIONS -->
<!-- DATA PERSISTENCE -->
<div class="text-muted" style="margin-bottom: 15px; margin-top: 25px">
<pr-icon icon="'database'" class="mr-1"></pr-icon>
Data persistence
</div>
<div class="small text-muted" ng-if="!ctrl.hasPersistedFolders()">
<pr-icon icon="'info'" mode="'primary'" class="mr-1"></pr-icon>
This application has no persisted folders.
</div>
<div ng-if="ctrl.hasPersistedFolders()">
<div class="small text-muted vertical-center" style="margin-bottom: 15px">
Data access policy:
<pr-icon icon="ctrl.application.DataAccessPolicy | kubernetesApplicationDataAccessPolicyIcon"></pr-icon>
{{ ctrl.application.DataAccessPolicy | kubernetesApplicationDataAccessPolicyText }}
<portainer-tooltip position="'right'" message="ctrl.application.DataAccessPolicy | kubernetesApplicationDataAccessPolicyTooltip"> </portainer-tooltip>
</div>
<table class="table" ng-if="ctrl.application.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED">
<tr class="text-muted">
<td style="width: 33%">Persisted folder</td>
<td style="width: 66%">Persistence</td>
</tr>
<tbody ng-repeat="container in ctrl.application.Containers" style="border-top: 0">
<tr ng-repeat="volume in container.PersistedFolders track by $index">
<td data-cy="k8sAppDetail-volMountPath">
{{ volume.MountPath }}
</td>
<td ng-if="volume.PersistentVolumeClaimName">
<a
class="hyperlink"
ui-sref="kubernetes.volumes.volume({ name: volume.PersistentVolumeClaimName, namespace: ctrl.application.ResourcePool })"
data-cy="k8sAppDetail-volClaimName"
><pr-icon icon="'database'" class="mr-1"></pr-icon>{{ volume.PersistentVolumeClaimName }}</a
>
</td>
<td ng-if="volume.HostPath"> {{ volume.HostPath }} on host filesystem </td>
</tr>
</tbody>
</table>
<table class="table" ng-if="ctrl.application.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED">
<thead>
<tr class="text-muted">
<td style="width: 25%">Container name</td>
<td style="width: 25%">Pod name</td>
<td style="width: 25%">Persisted folder</td>
<td style="width: 25%">Persistence</td>
</tr>
</thead>
<tbody ng-repeat="container in ctrl.allContainers track by $index" style="border-top: none">
<tr ng-repeat="volume in container.PersistedFolders track by $index">
<td>
{{ container.Name }}
<span ng-if="container.Type === ctrl.KubernetesPodContainerTypes.INIT"
><pr-icon icon="'asterisk'"></pr-icon> {{ envvar.valueFrom.fieldRef.fieldPath }} (<a
href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/"
target="_blank"
>init container</a
>)</span
>
</td>
<td>{{ container.PodName }}</td>
<td>
{{ volume.MountPath }}
</td>
<td ng-if="volume.PersistentVolumeClaimName">
<a
class="hyperlink"
ui-sref="kubernetes.volumes.volume({ name: volume.PersistentVolumeClaimName + '-' + container.PodName, namespace: ctrl.application.ResourcePool })"
>
<pr-icon icon="'database'" class="mr-1"></pr-icon>{{ volume.PersistentVolumeClaimName + '-' + container.PodName }}</a
>
</td>
<td ng-if="volume.HostPath"> {{ volume.HostPath }} on host filesystem </td>
</tr>
</tbody>
</table>
<!-- !DATA PERSISTENCE -->
</div>
</rd-widget-body>
</rd-widget>
<application-details-widget></application-details-widget>
</div>
</div>

View file

@ -15,9 +15,6 @@ 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';
import { confirmUpdate, confirm } from '@@/modals/confirm';
import { buildConfirmButton } from '@@/modals/utils';
import { ModalType } from '@@/modals';
function computeTolerations(nodes, application) {
const pod = application.Pods[0];
@ -146,11 +143,6 @@ class KubernetesApplicationController {
this.getApplicationAsync = this.getApplicationAsync.bind(this);
this.getEvents = this.getEvents.bind(this);
this.getEventsAsync = this.getEventsAsync.bind(this);
this.updateApplicationKindText = this.updateApplicationKindText.bind(this);
this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
this.redeployApplicationAsync = this.redeployApplicationAsync.bind(this);
this.rollbackApplicationAsync = this.rollbackApplicationAsync.bind(this);
this.copyLoadBalancerIP = this.copyLoadBalancerIP.bind(this);
}
selectTab(index) {
@ -166,128 +158,10 @@ class KubernetesApplicationController {
return KubernetesNamespaceHelper.isSystemNamespace(this.application.ResourcePool);
}
isExternalApplication() {
return KubernetesApplicationHelper.isExternalApplication(this.application);
}
copyLoadBalancerIP() {
this.clipboard.copyText(this.application.LoadBalancerIPAddress);
$('#copyNotificationLB').show().fadeOut(2500);
}
copyApplicationName() {
this.clipboard.copyText(this.application.Name);
$('#copyNotificationApplicationName').show().fadeOut(2500);
}
hasPersistedFolders() {
return this.application && this.application.PersistedFolders.length;
}
hasVolumeConfiguration() {
return this.application && this.application.ConfigurationVolumes.length;
}
hasEventWarnings() {
return this.state.eventWarningCount;
}
buildIngressRuleURL(rule) {
const hostname = rule.Host ? rule.Host : rule.IP;
return 'http://' + hostname + rule.Path;
}
portHasIngressRules(port) {
return port.IngressRules.length > 0;
}
ruleCanBeDisplayed(rule) {
return !rule.Host && !rule.IP ? false : true;
}
isStack() {
return this.application.StackId;
}
/**
* ROLLBACK
*/
async rollbackApplicationAsync() {
try {
// await this.KubernetesApplicationService.rollback(this.application, this.formValues.SelectedRevision);
const revision = _.nth(this.application.Revisions, -2);
await this.KubernetesApplicationService.rollback(this.application, revision);
this.Notifications.success('Success', 'Application successfully rolled back');
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to rollback the application');
}
}
rollbackApplication() {
confirmUpdate('Rolling back the application to a previous configuration may cause service interruption. Do you wish to continue?', (confirmed) => {
if (confirmed) {
return this.$async(this.rollbackApplicationAsync);
}
});
}
/**
* REDEPLOY
*/
async redeployApplicationAsync() {
const confirmed = await confirm({
modalType: ModalType.Warn,
title: 'Are you sure?',
message: 'Redeploying the application may cause a service interruption. Do you wish to continue?',
confirmButton: buildConfirmButton('Redeploy'),
});
if (!confirmed) {
return;
}
try {
const promises = _.map(this.application.Pods, (item) => this.KubernetesPodService.delete(item));
await Promise.all(promises);
this.Notifications.success('Success', 'Application successfully redeployed');
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to redeploy the application');
}
}
redeployApplication() {
return this.$async(this.redeployApplicationAsync);
}
/**
* UPDATE
*/
async updateApplicationAsync() {
try {
const application = angular.copy(this.application);
application.Note = this.formValues.Note;
await this.KubernetesApplicationService.patch(this.application, application, true);
this.Notifications.success('Success', 'Application successfully updated');
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to update application');
}
}
updateApplication() {
return this.$async(this.updateApplicationAsync);
}
updateApplicationKindText() {
if (this.application.ApplicationKind === this.KubernetesDeploymentTypes.GIT) {
this.state.appType = `git repository`;
} else if (this.application.ApplicationKind === this.KubernetesDeploymentTypes.CONTENT) {
this.state.appType = `manifest`;
} else if (this.application.ApplicationKind === this.KubernetesDeploymentTypes.URL) {
this.state.appType = `manifest`;
}
}
/**
* EVENTS
*/
@ -325,22 +199,7 @@ class KubernetesApplicationController {
this.KubernetesNodeService.get(),
]);
this.application = application;
if (this.application.StackId) {
this.stack = await this.StackService.stack(application.StackId);
}
this.allContainers = KubernetesApplicationHelper.associateAllContainersAndApplication(application);
this.formValues.Note = this.application.Note;
this.formValues.Services = this.application.Services;
if (this.application.Note) {
this.state.expandedNote = true;
}
if (this.application.CurrentRevision) {
this.formValues.SelectedRevision = _.find(this.application.Revisions, { revision: this.application.CurrentRevision.revision });
}
this.state.useIngress = _.find(application.PublishedPorts, (p) => {
return this.portHasIngressRules(p);
});
this.placements = computePlacements(nodes, this.application);
this.state.placementWarning = _.find(this.placements, { AcceptsApplication: true }) ? false : true;
@ -379,7 +238,6 @@ class KubernetesApplicationController {
eventWarningCount: 0,
placementWarning: false,
expandedNote: false,
useIngress: false,
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
publicUrl: this.endpoint.PublicURL,
};
@ -391,12 +249,8 @@ class KubernetesApplicationController {
SelectedRevision: undefined,
};
const resourcePools = await this.KubernetesResourcePoolService.get();
this.allNamespaces = resourcePools.map(({ Namespace }) => Namespace.Name);
await this.getApplication();
await this.getEvents();
this.updateApplicationKindText();
this.state.viewReady = true;
}

View file

@ -1,30 +0,0 @@
import _ from 'lodash-es';
export default class KubernetesApplicationIngressController {
/* @ngInject */
constructor($async, KubernetesIngressService) {
this.$async = $async;
this.KubernetesIngressService = KubernetesIngressService;
}
$onInit() {
return this.$async(async () => {
this.hasIngress;
this.applicationIngress = [];
const ingresses = await this.KubernetesIngressService.get(this.application.ResourcePool);
const services = this.application.Services;
_.forEach(services, (service) => {
_.forEach(ingresses, (ingress) => {
_.forEach(ingress.Paths, (path) => {
if (path.ServiceName === service.metadata.name) {
path.Secure = ingress.TLS && ingress.TLS.filter((tls) => tls.hosts && tls.hosts.includes(path.Host)).length > 0;
this.applicationIngress.push(path);
this.hasIngress = true;
}
});
});
});
});
}
}

View file

@ -1,28 +0,0 @@
<div style="margin-top: 15px" ng-if="$ctrl.hasIngress">
<table class="table">
<tbody>
<tr class="text-muted">
<td style="width: 15%">Ingress name</td>
<td style="width: 10%">Service name</td>
<td style="width: 10%">Host</td>
<td style="width: 10%">Port</td>
<td style="width: 10%">Path</td>
<td style="width: 15%">HTTP Route</td>
</tr>
<tr ng-repeat="ingress in $ctrl.applicationIngress">
<td
><a authorization="K8sIngressesW" ui-sref="kubernetes.ingresses.edit({ name: ingress.IngressName, namespace: $ctrl.application.ResourcePool })">{{
ingress.IngressName
}}</a></td
>
<td>{{ ingress.ServiceName }}</td>
<td>{{ ingress.Host }}</td>
<td>{{ ingress.Port }}</td>
<td>{{ ingress.Path }}</td>
<td
><a target="_blank" href="{{ ingress.Secure ? 'https' : 'http' }}://{{ ingress.Host }}{{ ingress.Path }}">{{ ingress.Host }}{{ ingress.Path }}</a></td
>
</tr>
</tbody>
</table>
</div>

View file

@ -1,11 +0,0 @@
import angular from 'angular';
import controller from './ingress-table.controller';
angular.module('portainer.kubernetes').component('kubernetesApplicationIngressTable', {
templateUrl: './ingress-table.html',
controller,
bindings: {
application: '<',
publicUrl: '<',
},
});

View file

@ -1,62 +0,0 @@
<!-- table -->
<div style="margin-top: 15px">
<table class="table">
<tbody>
<tr class="text-muted">
<td style="width: 15%">Service name</td>
<td style="width: 10%">Type</td>
<td style="width: 10%">Cluster IP</td>
<td style="width: 10%">External IP</td>
<td style="width: 10%">Container port</td>
<td style="width: 15%">Service port(s)</td>
</tr>
<tr ng-repeat="service in $ctrl.services">
<td>{{ service.metadata.name }}</td>
<td>{{ service.spec.type }}</td>
<td>{{ service.spec.clusterIP }}</td>
<td ng-show="service.spec.type === 'LoadBalancer'">
<div ng-show="service.status.loadBalancer.ingress">
<a class="vertical-center hyperlink" target="_blank" ng-href="http://{{ service.status.loadBalancer.ingress[0].ip }}:{{ service.spec.ports[0].port }}">
<pr-icon icon="'external-link'"></pr-icon>
<span data-cy="k8sAppDetail-containerPort"> Access </span>
</a>
</div>
<div ng-show="!service.status.loadBalancer.ingress">
{{ service.spec.externalIP ? service.spec.externalIP : 'pending...' }}
</div>
</td>
<td ng-show="service.spec.type !== 'LoadBalancer'">{{ service.spec.externalIP ? service.spec.externalIP : '-' }}</td>
<td data-cy="k8sAppDetail-containerPort">
<div ng-repeat="port in service.spec.ports">{{ port.targetPort }}</div>
</td>
<td ng-if="!ctrl.portHasIngressRules(port)">
<div ng-repeat="port in service.spec.ports">
<a
class="vertical-center hyperlink"
ng-if="$ctrl.publicUrl && port.nodePort"
ng-href="http://{{ $ctrl.publicUrl }}:{{ port.nodePort }}"
target="_blank"
style="margin-left: 5px"
>
<pr-icon icon="'external-link'"></pr-icon>
<span data-cy="k8sAppDetail-containerPort">
{{ port.port }}
</span>
<span>{{ port.nodePort ? ':' : '' }}</span>
<span data-cy="k8sAppDetail-nodePort"> {{ port.nodePort }}/{{ port.protocol }} </span>
</a>
<div ng-if="!$ctrl.publicUrl">
<span data-cy="k8sAppDetail-servicePort">
{{ port.port }}
</span>
<span>{{ port.nodePort ? ':' : '' }}</span>
<span data-cy="k8sAppDetail-nodePort"> {{ port.nodePort }}/{{ port.protocol }} </span>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View file

@ -1,10 +0,0 @@
import angular from 'angular';
angular.module('portainer.kubernetes').component('kubernetesApplicationServicesTable', {
templateUrl: './services-table.html',
bindings: {
services: '<',
application: '<',
publicUrl: '<',
},
});