mirror of https://github.com/portainer/portainer
feat(UX): add an image autocomplete feature for services and containers (#1389)
parent
a2d41e5316
commit
6b9f3dad7a
|
@ -9,7 +9,7 @@
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal" autocomplete="off">
|
||||||
<!-- name-input -->
|
<!-- name-input -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
|
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
<div ng-if="formValues.Registry || !fromContainer">
|
<div ng-if="formValues.Registry || !fromContainer">
|
||||||
<!-- image-and-registry -->
|
<!-- image-and-registry -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry"></por-image-registry>
|
<por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry" auto-complete="true"></por-image-registry>
|
||||||
</div>
|
</div>
|
||||||
<!-- !image-and-registry -->
|
<!-- !image-and-registry -->
|
||||||
<!-- always-pull -->
|
<!-- always-pull -->
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal" autocomplete="off">
|
||||||
<!-- name-input -->
|
<!-- name-input -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="service_name" class="col-sm-1 control-label text-left">Name</label>
|
<label for="service_name" class="col-sm-1 control-label text-left">Name</label>
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- image-and-registry -->
|
<!-- image-and-registry -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<por-image-registry image="formValues.Image" registry="formValues.Registry"></por-image-registry>
|
<por-image-registry image="formValues.Image" registry="formValues.Registry" auto-complete="true"></por-image-registry>
|
||||||
</div>
|
</div>
|
||||||
<!-- !image-and-registry -->
|
<!-- !image-and-registry -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|
|
@ -68,7 +68,8 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>Image</td>
|
<td>Image</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" class="form-control" ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" ng-disabled="isUpdating" />
|
<input type="text" class="form-control" uib-typeahead="image for image in availableImages | filter:$viewValue | limitTo:5"
|
||||||
|
ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" id="image_name" ng-disabled="isUpdating">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="applicationState.endpoint.apiVersion >= 1.30">
|
<tr ng-if="applicationState.endpoint.apiVersion >= 1.30">
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
angular.module('service', [])
|
angular.module('service', [])
|
||||||
.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService',
|
.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService',
|
||||||
function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) {
|
function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) {
|
||||||
|
|
||||||
$scope.state = {};
|
$scope.state = {};
|
||||||
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
|
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
|
||||||
$scope.tasks = [];
|
$scope.tasks = [];
|
||||||
$scope.sortType = 'Updated';
|
$scope.sortType = 'Updated';
|
||||||
$scope.sortReverse = true;
|
$scope.sortReverse = true;
|
||||||
|
$scope.availableImages = [];
|
||||||
|
|
||||||
$scope.lastVersion = 0;
|
$scope.lastVersion = 0;
|
||||||
|
|
||||||
|
@ -333,7 +334,8 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
||||||
tasks: TaskService.tasks({ service: [service.Name] }),
|
tasks: TaskService.tasks({ service: [service.Name] }),
|
||||||
nodes: NodeService.nodes(),
|
nodes: NodeService.nodes(),
|
||||||
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
||||||
configs: apiVersion >= 1.30 ? ConfigService.configs() : []
|
configs: apiVersion >= 1.30 ? ConfigService.configs() : [],
|
||||||
|
availableImages: ImageService.images()
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
|
@ -341,6 +343,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
|
||||||
$scope.nodes = data.nodes;
|
$scope.nodes = data.nodes;
|
||||||
$scope.configs = data.configs;
|
$scope.configs = data.configs;
|
||||||
$scope.secrets = data.secrets;
|
$scope.secrets = data.secrets;
|
||||||
|
$scope.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
|
||||||
|
|
||||||
// Set max cpu value
|
// Set max cpu value
|
||||||
var maxCpus = 0;
|
var maxCpus = 0;
|
||||||
|
|
|
@ -3,6 +3,7 @@ angular.module('portainer').component('porImageRegistry', {
|
||||||
controller: 'porImageRegistryController',
|
controller: 'porImageRegistryController',
|
||||||
bindings: {
|
bindings: {
|
||||||
'image': '=',
|
'image': '=',
|
||||||
'registry': '='
|
'registry': '=',
|
||||||
|
'autoComplete': '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
<div>
|
<div>
|
||||||
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
|
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
|
||||||
<div class="col-sm-11 col-md-6">
|
<div class="col-sm-11 col-md-6">
|
||||||
<input type="text" class="form-control" ng-model="$ctrl.image" id="image_name" placeholder="e.g. myImage:myTag">
|
<input type="text" class="form-control" uib-typeahead="image for image in $ctrl.availableImages | filter:$viewValue | limitTo:5"
|
||||||
|
ng-model="$ctrl.image" id="image_name" placeholder="e.g. myImage:myTag">
|
||||||
</div>
|
</div>
|
||||||
<label for="image_registry" class="col-sm-2 col-md-1 margin-sm-top control-label text-left">
|
<label for="image_registry" class="col-sm-2 col-md-1 margin-sm-top control-label text-left">
|
||||||
Registry
|
Registry
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-10 col-md-4 margin-sm-top">
|
<div class="col-sm-10 col-md-4 margin-sm-top">
|
||||||
<select ng-options="registry as registry.Name for registry in $ctrl.availableRegistries" ng-model="$ctrl.registry" id="image_registry" class="form-control"></select>
|
<select ng-options="registry as registry.Name for registry in $ctrl.availableRegistries" ng-model="$ctrl.registry" id="image_registry"
|
||||||
|
class="form-control"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -1,27 +1,29 @@
|
||||||
angular.module('portainer')
|
angular.module('portainer')
|
||||||
.controller('porImageRegistryController', ['$q', 'RegistryService', 'DockerHubService', 'Notifications',
|
.controller('porImageRegistryController', ['$q', 'RegistryService', 'DockerHubService', 'ImageService', 'Notifications',
|
||||||
function ($q, RegistryService, DockerHubService, Notifications) {
|
function ($q, RegistryService, DockerHubService, ImageService, Notifications) {
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
function initComponent() {
|
function initComponent() {
|
||||||
$q.all({
|
$q.all({
|
||||||
registries: RegistryService.registries(),
|
registries: RegistryService.registries(),
|
||||||
dockerhub: DockerHubService.dockerhub()
|
dockerhub: DockerHubService.dockerhub(),
|
||||||
})
|
availableImages: ctrl.autoComplete ? ImageService.images() : []
|
||||||
.then(function success(data) {
|
})
|
||||||
var dockerhub = data.dockerhub;
|
.then(function success(data) {
|
||||||
var registries = data.registries;
|
var dockerhub = data.dockerhub;
|
||||||
ctrl.availableRegistries = [dockerhub].concat(registries);
|
var registries = data.registries;
|
||||||
if (!ctrl.registry.Id) {
|
ctrl.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
|
||||||
ctrl.registry = dockerhub;
|
ctrl.availableRegistries = [dockerhub].concat(registries);
|
||||||
} else {
|
if (!ctrl.registry.Id) {
|
||||||
ctrl.registry = _.find(ctrl.availableRegistries, { 'Id': ctrl.registry.Id });
|
ctrl.registry = dockerhub;
|
||||||
|
} else {
|
||||||
|
ctrl.registry = _.find(ctrl.availableRegistries, { 'Id': ctrl.registry.Id });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve registries');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve registries');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initComponent();
|
initComponent();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -152,5 +152,14 @@ angular.module('portainer.services')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.getUniqueTagListFromImages = function (availableImages) {
|
||||||
|
return _.flatten(_.map(availableImages, function (image) {
|
||||||
|
_.remove(image.RepoTags, function (item) {
|
||||||
|
return item.indexOf('<none>') !== -1;
|
||||||
|
});
|
||||||
|
return image.RepoTags ? _.uniqWith(image.RepoTags, _.isEqual) : [];
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
Loading…
Reference in New Issue