mirror of https://github.com/portainer/portainer
feat(endpoints): UX enhancements (#1943)
* feat(endpoints): add details about endpoints in datatable * feat(endpoint-details): add the ability to inspect/update azure endpoint * feat(endpoint-selector): disable placeholder selectionpull/1944/head
parent
bfc49574b7
commit
9bb885629a
|
@ -68,13 +68,16 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
putEndpointsRequest struct {
|
putEndpointsRequest struct {
|
||||||
Name string `valid:"-"`
|
Name string `valid:"-"`
|
||||||
URL string `valid:"-"`
|
URL string `valid:"-"`
|
||||||
PublicURL string `valid:"-"`
|
PublicURL string `valid:"-"`
|
||||||
GroupID int `valid:"-"`
|
GroupID int `valid:"-"`
|
||||||
TLS bool `valid:"-"`
|
TLS bool `valid:"-"`
|
||||||
TLSSkipVerify bool `valid:"-"`
|
TLSSkipVerify bool `valid:"-"`
|
||||||
TLSSkipClientVerify bool `valid:"-"`
|
TLSSkipClientVerify bool `valid:"-"`
|
||||||
|
AzureApplicationID string `valid:"-"`
|
||||||
|
AzureTenantID string `valid:"-"`
|
||||||
|
AzureAuthenticationKey string `valid:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
postEndpointPayload struct {
|
postEndpointPayload struct {
|
||||||
|
@ -143,7 +146,7 @@ func (handler *EndpointHandler) createAzureEndpoint(payload *postEndpointPayload
|
||||||
|
|
||||||
endpoint := &portainer.Endpoint{
|
endpoint := &portainer.Endpoint{
|
||||||
Name: payload.name,
|
Name: payload.name,
|
||||||
URL: payload.url,
|
URL: proxy.AzureAPIBaseURL,
|
||||||
Type: portainer.AzureEnvironment,
|
Type: portainer.AzureEnvironment,
|
||||||
GroupID: portainer.EndpointGroupID(payload.groupID),
|
GroupID: portainer.EndpointGroupID(payload.groupID),
|
||||||
PublicURL: payload.publicURL,
|
PublicURL: payload.publicURL,
|
||||||
|
@ -405,8 +408,6 @@ func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint.AzureCredentials = portainer.AzureCredentials{}
|
|
||||||
|
|
||||||
encodeJSON(w, endpoint, handler.Logger)
|
encodeJSON(w, endpoint, handler.Logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -518,6 +519,18 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
||||||
endpoint.GroupID = portainer.EndpointGroupID(req.GroupID)
|
endpoint.GroupID = portainer.EndpointGroupID(req.GroupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if endpoint.Type == portainer.AzureEnvironment {
|
||||||
|
if req.AzureApplicationID != "" {
|
||||||
|
endpoint.AzureCredentials.ApplicationID = req.AzureApplicationID
|
||||||
|
}
|
||||||
|
if req.AzureTenantID != "" {
|
||||||
|
endpoint.AzureCredentials.TenantID = req.AzureTenantID
|
||||||
|
}
|
||||||
|
if req.AzureAuthenticationKey != "" {
|
||||||
|
endpoint.AzureCredentials.AuthenticationKey = req.AzureAuthenticationKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
folder := strconv.Itoa(int(endpoint.ID))
|
folder := strconv.Itoa(int(endpoint.ID))
|
||||||
if req.TLS {
|
if req.TLS {
|
||||||
endpoint.TLSConfig.TLS = true
|
endpoint.TLSConfig.TLS = true
|
||||||
|
|
|
@ -10,7 +10,8 @@ import (
|
||||||
"github.com/portainer/portainer/crypto"
|
"github.com/portainer/portainer/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const azureAPIBaseURL = "https://management.azure.com"
|
// AzureAPIBaseURL is the URL where Azure API requests will be proxied.
|
||||||
|
const AzureAPIBaseURL = "https://management.azure.com"
|
||||||
|
|
||||||
// proxyFactory is a factory to create reverse proxies to Docker endpoints
|
// proxyFactory is a factory to create reverse proxies to Docker endpoints
|
||||||
type proxyFactory struct {
|
type proxyFactory struct {
|
||||||
|
@ -28,7 +29,7 @@ func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error) {
|
func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error) {
|
||||||
url, err := url.Parse(azureAPIBaseURL)
|
url, err := url.Parse(AzureAPIBaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
angular.module('portainer.azure').component('azureEndpointConfig', {
|
||||||
|
bindings: {
|
||||||
|
applicationId: '=',
|
||||||
|
tenantId: '=',
|
||||||
|
authenticationKey: '='
|
||||||
|
},
|
||||||
|
templateUrl: 'app/azure/components/azure-endpoint-config/azureEndpointConfig.html'
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
<div>
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Azure configuration
|
||||||
|
</div>
|
||||||
|
<!-- applicationId-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="azure_credential_appid" class="col-sm-3 col-lg-2 control-label text-left">Application ID</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" name="azure_credential_appid" ng-model="$ctrl.applicationId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !applicationId-input -->
|
||||||
|
<!-- tenantId-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="azure_credential_tenantid" class="col-sm-3 col-lg-2 control-label text-left">Tenant ID</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" name="azure_credential_tenantid" ng-model="$ctrl.tenantId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !tenantId-input -->
|
||||||
|
<!-- authenticationkey-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="azure_credential_authkey" class="col-sm-3 col-lg-2 control-label text-left">Authentication key</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" name="azure_credential_authkey" ng-model="$ctrl.authenticationKey" placeholder="cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !authenticationkey-input -->
|
||||||
|
</div>
|
|
@ -39,6 +39,13 @@
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Type')">
|
||||||
|
Type
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ng-click="$ctrl.changeOrderBy('URL')">
|
<a ng-click="$ctrl.changeOrderBy('URL')">
|
||||||
URL
|
URL
|
||||||
|
@ -66,6 +73,12 @@
|
||||||
<a ui-sref="portainer.endpoints.endpoint({id: item.Id})" ng-if="$ctrl.endpointManagement">{{ item.Name }}</a>
|
<a ui-sref="portainer.endpoints.endpoint({id: item.Id})" ng-if="$ctrl.endpointManagement">{{ item.Name }}</a>
|
||||||
<span ng-if="!$ctrl.endpointManagement">{{ item.Name }}</span>
|
<span ng-if="!$ctrl.endpointManagement">{{ item.Name }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>
|
||||||
|
<i ng-class="item.Type | endpointtypeicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
{{ item.Type | endpointtypename }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td>{{ item.URL | stripprotocol }}</td>
|
<td>{{ item.URL | stripprotocol }}</td>
|
||||||
<td>{{ item.GroupName }}</td>
|
<td>{{ item.GroupName }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<li class="sidebar-title"><span>Group</span></li>
|
<li class="sidebar-title"><span>Group</span></li>
|
||||||
<li class="sidebar-title">
|
<li class="sidebar-title">
|
||||||
<select class="select-endpoint form-control" ng-options="group.Name for group in $ctrl.availableGroups" ng-model="$ctrl.state.selectedGroup" ng-change="$ctrl.selectGroup()">
|
<select class="select-endpoint form-control" ng-options="group.Name for group in $ctrl.availableGroups" ng-model="$ctrl.state.selectedGroup" ng-change="$ctrl.selectGroup()">
|
||||||
<option value="">Select a group</option>
|
<option value="" disabled selected>Select a group</option>
|
||||||
</select>
|
</select>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
<li class="sidebar-title"><span>Endpoint</span></li>
|
<li class="sidebar-title"><span>Endpoint</span></li>
|
||||||
<li class="sidebar-title">
|
<li class="sidebar-title">
|
||||||
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in $ctrl.availableEndpoints" ng-model="$ctrl.state.selectedEndpoint" ng-change="$ctrl.selectEndpoint($ctrl.state.selectedEndpoint)">
|
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in $ctrl.availableEndpoints" ng-model="$ctrl.state.selectedEndpoint" ng-change="$ctrl.selectEndpoint($ctrl.state.selectedEndpoint)">
|
||||||
<option value="">Select an endpoint</option>
|
<option value="" disabled selected>Select an endpoint</option>
|
||||||
</select>
|
</select>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -102,6 +102,28 @@ angular.module('portainer.app')
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
.filter('endpointtypename', function () {
|
||||||
|
'use strict';
|
||||||
|
return function (type) {
|
||||||
|
if (type === 1) {
|
||||||
|
return 'Docker';
|
||||||
|
} else if (type === 2) {
|
||||||
|
return 'Agent';
|
||||||
|
} else if (type === 3) {
|
||||||
|
return 'Azure ACI';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter('endpointtypeicon', function () {
|
||||||
|
'use strict';
|
||||||
|
return function (type) {
|
||||||
|
if (type === 3) {
|
||||||
|
return 'fab fa-microsoft';
|
||||||
|
}
|
||||||
|
return 'fab fa-docker';
|
||||||
|
};
|
||||||
|
})
|
||||||
.filter('ownershipicon', function () {
|
.filter('ownershipicon', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (ownership) {
|
return function (ownership) {
|
||||||
|
|
|
@ -33,25 +33,12 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
return Endpoints.updateAccess({id: id}, {authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs}).$promise;
|
return Endpoints.updateAccess({id: id}, {authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs}).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.updateEndpoint = function(id, endpointParams) {
|
service.updateEndpoint = function(id, payload) {
|
||||||
var query = {
|
|
||||||
name: endpointParams.name,
|
|
||||||
PublicURL: endpointParams.PublicURL,
|
|
||||||
GroupId: endpointParams.GroupId,
|
|
||||||
TLS: endpointParams.TLS,
|
|
||||||
TLSSkipVerify: endpointParams.TLSSkipVerify,
|
|
||||||
TLSSkipClientVerify: endpointParams.TLSSkipClientVerify,
|
|
||||||
authorizedUsers: endpointParams.authorizedUsers
|
|
||||||
};
|
|
||||||
if (endpointParams.type && endpointParams.URL) {
|
|
||||||
query.URL = endpointParams.type === 'local' ? ('unix://' + endpointParams.URL) : ('tcp://' + endpointParams.URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
FileUploadService.uploadTLSFilesForEndpoint(id, endpointParams.TLSCACert, endpointParams.TLSCert, endpointParams.TLSKey)
|
FileUploadService.uploadTLSFilesForEndpoint(id, payload.TLSCACert, payload.TLSCert, payload.TLSKey)
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
deferred.notify({upload: false});
|
deferred.notify({upload: false});
|
||||||
return Endpoints.update({id: id}, query).$promise;
|
return Endpoints.update({id: id}, payload).$promise;
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
deferred.resolve(data);
|
deferred.resolve(data);
|
||||||
|
|
|
@ -28,12 +28,12 @@
|
||||||
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
|
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-9 col-lg-10">
|
<div class="col-sm-9 col-lg-10">
|
||||||
<input ng-disabled="endpointType === 'local'" type="text" class="form-control" id="endpoint_url" ng-model="endpoint.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
|
<input ng-disabled="endpointType === 'local' || endpoint.Type === 3" type="text" class="form-control" id="endpoint_url" ng-model="endpoint.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !endpoint-url-input -->
|
<!-- !endpoint-url-input -->
|
||||||
<!-- endpoint-public-url-input -->
|
<!-- endpoint-public-url-input -->
|
||||||
<div class="form-group">
|
<div class="form-group" ng-if="endpoint.Type !== 3">
|
||||||
<label for="endpoint_public_url" class="col-sm-3 col-lg-2 control-label text-left">
|
<label for="endpoint_public_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
Public IP
|
Public IP
|
||||||
<portainer-tooltip position="bottom" message="URL or IP address where exposed containers will be reachable. This field is optional and will default to the endpoint URL."></portainer-tooltip>
|
<portainer-tooltip position="bottom" message="URL or IP address where exposed containers will be reachable. This field is optional and will default to the endpoint URL."></portainer-tooltip>
|
||||||
|
@ -42,6 +42,11 @@
|
||||||
<input type="text" class="form-control" id="endpoint_public_url" ng-model="endpoint.PublicURL" placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com">
|
<input type="text" class="form-control" id="endpoint_public_url" ng-model="endpoint.PublicURL" placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<azure-endpoint-config ng-if="endpoint.Type === 3"
|
||||||
|
application-id="endpoint.AzureCredentials.ApplicationID"
|
||||||
|
tenant-id="endpoint.AzureCredentials.TenantID"
|
||||||
|
authentication-key="endpoint.AzureCredentials.AuthenticationKey"
|
||||||
|
></azure-endpoint-config>
|
||||||
<!-- !endpoint-public-url-input -->
|
<!-- !endpoint-public-url-input -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Grouping
|
Grouping
|
||||||
|
@ -57,7 +62,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !group -->
|
<!-- !group -->
|
||||||
<!-- endpoint-security -->
|
<!-- endpoint-security -->
|
||||||
<div ng-if="endpointType === 'remote'">
|
<div ng-if="endpointType === 'remote' && endpoint.Type !== 3">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Security
|
Security
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,22 +23,27 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi
|
||||||
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||||
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||||
|
|
||||||
var endpointParams = {
|
var payload = {
|
||||||
name: endpoint.Name,
|
Name: endpoint.Name,
|
||||||
URL: endpoint.URL,
|
|
||||||
PublicURL: endpoint.PublicURL,
|
PublicURL: endpoint.PublicURL,
|
||||||
GroupId: endpoint.GroupId,
|
GroupID: endpoint.GroupId,
|
||||||
TLS: TLS,
|
TLS: TLS,
|
||||||
TLSSkipVerify: TLSSkipVerify,
|
TLSSkipVerify: TLSSkipVerify,
|
||||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||||
TLSCACert: TLSSkipVerify || securityData.TLSCACert === endpoint.TLSConfig.TLSCACert ? null : securityData.TLSCACert,
|
TLSCACert: TLSSkipVerify || securityData.TLSCACert === endpoint.TLSConfig.TLSCACert ? null : securityData.TLSCACert,
|
||||||
TLSCert: TLSSkipClientVerify || securityData.TLSCert === endpoint.TLSConfig.TLSCert ? null : securityData.TLSCert,
|
TLSCert: TLSSkipClientVerify || securityData.TLSCert === endpoint.TLSConfig.TLSCert ? null : securityData.TLSCert,
|
||||||
TLSKey: TLSSkipClientVerify || securityData.TLSKey === endpoint.TLSConfig.TLSKey ? null : securityData.TLSKey,
|
TLSKey: TLSSkipClientVerify || securityData.TLSKey === endpoint.TLSConfig.TLSKey ? null : securityData.TLSKey,
|
||||||
type: $scope.endpointType
|
AzureApplicationID: endpoint.AzureCredentials.ApplicationID,
|
||||||
|
AzureTenantID: endpoint.AzureCredentials.TenantID,
|
||||||
|
AzureAuthenticationKey: endpoint.AzureCredentials.AuthenticationKey
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($scope.endpointType !== 'local' && endpoint.Type !== 3) {
|
||||||
|
payload.URL = 'tcp://' + endpoint.URL;
|
||||||
|
}
|
||||||
|
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
EndpointService.updateEndpoint(endpoint.Id, endpointParams)
|
EndpointService.updateEndpoint(endpoint.Id, payload)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
||||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||||
|
|
Loading…
Reference in New Issue