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 selection
pull/1944/head
Anthony Lapenna 2018-06-01 16:13:24 +02:00 committed by GitHub
parent bfc49574b7
commit 9bb885629a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 122 additions and 39 deletions

View File

@ -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

View File

@ -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
} }

View File

@ -0,0 +1,8 @@
angular.module('portainer.azure').component('azureEndpointConfig', {
bindings: {
applicationId: '=',
tenantId: '=',
authenticationKey: '='
},
templateUrl: 'app/azure/components/azure-endpoint-config/azureEndpointConfig.html'
});

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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) {

View File

@ -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);

View File

@ -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>

View File

@ -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);