mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
feat(extensions): introduce extension support (#2527)
* wip * wip: missing repository & tags removal * feat(registry): private registry management * style(plugin-details): update view * wip * wip * wip * feat(plugins): add license info * feat(plugins): browse feature preview * feat(registry-configure): add the ability to configure registry management * style(app): update text in app * feat(plugins): add plugin version number * feat(plugins): wip plugin upgrade process * feat(plugins): wip plugin upgrade * feat(plugins): add the ability to update a plugin * feat(plugins): init plugins at startup time * feat(plugins): add the ability to remove a plugin * feat(plugins): update to latest plugin definitions * feat(plugins): introduce plugin-tooltip component * refactor(app): relocate plugin files to app/plugins * feat(plugins): introduce PluginDefinitionsURL constant * feat(plugins): update the flags used by the plugins * feat(plugins): wip * feat(plugins): display a label when a plugin has expired * wip * feat(registry-creation): update registry creation logic * refactor(registry-creation): change name/ids for inputs * feat(api): pass registry type to management configuration * feat(api): unstrip /v2 in regsitry proxy * docs(api): add TODO * feat(store): mockup-1 * feat(store): mockup 2 * feat(store): mockup 2 * feat(store): update mockup-2 * feat(app): add unauthenticated event check * update gruntfile * style(support): update support views * style(support): update product views * refactor(extensions): refactor plugins to extensions * feat(extensions): add a deal property * feat(extensions): introduce ExtensionManager * style(extensions): update extension details style * feat(extensions): display license/company when enabling extension * feat(extensions): update extensions views * feat(extensions): use ProductId defined in extension schema * style(app): remove padding left for form section title elements * style(support): use per host model * refactor(extensions): multiple refactors related to extensions mecanism * feat(extensions): update tls file path for registry extension * feat(extensions): update registry management configuration * feat(extensions): send license in header to extension proxy * fix(proxy): fix invalid default loopback address * feat(extensions): add header X-RegistryManagement-ForceNew for specific operations * feat(extensions): add the ability to display screenshots * feat(extensions): center screenshots * style(extensions): tune style * feat(extensions-details): open full screen image on click (#2517) * feat(extension-details): show magnifying glass on images * feat(extensions): support extension logo * feat(extensions): update support logos * refactor(lint): fix lint issues
This commit is contained in:
parent
f5dc663879
commit
6fd5ddc802
100 changed files with 3519 additions and 268 deletions
3
app/extensions/_module.js
Normal file
3
app/extensions/_module.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
angular.module('portainer.extensions', [
|
||||
'portainer.extensions.registrymanagement'
|
||||
]);
|
41
app/extensions/registry-management/_module.js
Normal file
41
app/extensions/registry-management/_module.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
angular.module('portainer.extensions.registrymanagement', [])
|
||||
.config(['$stateRegistryProvider', function ($stateRegistryProvider) {
|
||||
'use strict';
|
||||
|
||||
var registryConfiguration = {
|
||||
name: 'portainer.registries.registry.configure',
|
||||
url: '/configure',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/extensions/registry-management/views/configure/configureregistry.html',
|
||||
controller: 'ConfigureRegistryController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var registryRepositories = {
|
||||
name: 'portainer.registries.registry.repositories',
|
||||
url: '/repositories',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/extensions/registry-management/views/repositories/registryRepositories.html',
|
||||
controller: 'RegistryRepositoriesController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var registryRepositoryTags = {
|
||||
name: 'portainer.registries.registry.repository',
|
||||
url: '/:repository',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/extensions/registry-management/views/repositories/edit/registryRepository.html',
|
||||
controller: 'RegistryRepositoryController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(registryConfiguration);
|
||||
$stateRegistryProvider.register(registryRepositories);
|
||||
$stateRegistryProvider.register(registryRepositoryTags);
|
||||
}]);
|
|
@ -0,0 +1,83 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-filters nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Repository
|
||||
<i class="fa fa-sort-alpha-down" 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>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('TagsCount')">
|
||||
Tags count
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'TagsCount' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'TagsCount' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
|
||||
ng-class="{active: item.Checked}">
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)" />
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="portainer.registries.registry.repository({repository: item.Name})" class="monospaced"
|
||||
title="{{ item.Name }}">{{ item.Name }}</a>
|
||||
</td>
|
||||
<td>{{ item.TagsCount }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="5" class="text-center text-muted">No repository available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||
</div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('portainer.extensions.registrymanagement').component('registryRepositoriesDatatable', {
|
||||
templateUrl: 'app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,112 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{
|
||||
$ctrl.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-down" 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>
|
||||
</th>
|
||||
<th>Os/Architecture</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('ImageId')">
|
||||
Image ID
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ImageId' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ImageId' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Size')">
|
||||
Size
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Size' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Size' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
|
||||
ng-class="{active: item.Checked}">
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)" />
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
{{ item.Name }}
|
||||
</td>
|
||||
<td>{{ item.Os }}/{{ item.Architecture }}</td>
|
||||
<td>{{ item.ImageId | truncate:40 }}</td>
|
||||
<td>{{ item.Size | humansize }}</td>
|
||||
<td>
|
||||
<span ng-if="!item.Modified">
|
||||
<a class="interactive" ng-click="item.Modified = true; item.NewName = item.Name; $event.stopPropagation();">
|
||||
<i class="fa fa-tag" aria-hidden="true"></i> Retag
|
||||
</a>
|
||||
</span>
|
||||
<span ng-if="item.Modified">
|
||||
<input class="input-sm" type="text" ng-model="item.NewName" on-enter-key="$ctrl.retagAction(item)"
|
||||
auto-focus ng-click="$event.stopPropagation();" />
|
||||
<a class="interactive" ng-click="item.Modified = false; $event.stopPropagation();"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="$ctrl.retagAction(item); $event.stopPropagation();"><i class="fa fa-check-square"></i></a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="3" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="3" class="text-center text-muted">No tag available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||
</div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
angular.module('portainer.extensions.registrymanagement').component('registriesRepositoryTagsDatatable', {
|
||||
templateUrl: 'app/extensions/registry-management/components/registries-repository-tags-datatable/registriesRepositoryTagsDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
retagAction: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
angular.module('portainer.extensions.registrymanagement')
|
||||
.factory('RegistryV2Helper', [function RegistryV2HelperFactory() {
|
||||
'use strict';
|
||||
|
||||
var helper = {};
|
||||
|
||||
function historyRawToParsed(rawHistory) {
|
||||
var history = [];
|
||||
for (var i = 0; i < rawHistory.length; i++) {
|
||||
var item = rawHistory[i];
|
||||
history.push(angular.fromJson(item.v1Compatibility));
|
||||
}
|
||||
return history;
|
||||
}
|
||||
|
||||
helper.manifestsToTag = function (manifests) {
|
||||
var v1 = manifests.v1;
|
||||
var v2 = manifests.v2;
|
||||
|
||||
var history = historyRawToParsed(v1.history);
|
||||
var imageId = history[0].id;
|
||||
var name = v1.tag;
|
||||
var os = history[0].os;
|
||||
var arch = v1.architecture;
|
||||
var size = v2.layers.reduce(function (a, b) {
|
||||
return {
|
||||
size: a.size + b.size
|
||||
};
|
||||
}).size;
|
||||
var digest = v2.digest;
|
||||
var repositoryName = v1.name;
|
||||
var fsLayers = v1.fsLayers;
|
||||
|
||||
return new RepositoryTagViewModel(name, imageId, os, arch, size, digest, repositoryName, fsLayers, history, v2);
|
||||
};
|
||||
|
||||
return helper;
|
||||
}]);
|
|
@ -0,0 +1,4 @@
|
|||
function RegistryRepositoryViewModel(data) {
|
||||
this.Name = data.name;
|
||||
this.TagsCount = data.tags.length;
|
||||
}
|
12
app/extensions/registry-management/models/repositoryTag.js
Normal file
12
app/extensions/registry-management/models/repositoryTag.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
function RepositoryTagViewModel(name, imageId, os, arch, size, digest, repositoryName, fsLayers, history, manifestv2) {
|
||||
this.Name = name;
|
||||
this.ImageId = imageId;
|
||||
this.Os = os;
|
||||
this.Architecture = arch;
|
||||
this.Size = size;
|
||||
this.Digest = digest;
|
||||
this.RepositoryName = repositoryName;
|
||||
this.FsLayers = fsLayers;
|
||||
this.History = history;
|
||||
this.ManifestV2 = manifestv2;
|
||||
}
|
23
app/extensions/registry-management/rest/catalog.js
Normal file
23
app/extensions/registry-management/rest/catalog.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
angular.module('portainer.extensions.registrymanagement')
|
||||
.factory('RegistryCatalog', ['$resource', 'API_ENDPOINT_REGISTRIES', function RegistryCatalogFactory($resource, API_ENDPOINT_REGISTRIES) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_REGISTRIES + '/:id/v2/:action', {},
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
params: { id: '@id', action: '_catalog' }
|
||||
},
|
||||
ping: {
|
||||
method: 'GET',
|
||||
params: { id: '@id' }, timeout: 3500
|
||||
},
|
||||
pingWithForceNew: {
|
||||
method: 'GET',
|
||||
params: { id: '@id' }, timeout: 3500,
|
||||
headers: { 'X-RegistryManagement-ForceNew': '1' }
|
||||
}
|
||||
},
|
||||
{
|
||||
stripTrailingSlashes: false
|
||||
});
|
||||
}]);
|
61
app/extensions/registry-management/rest/manifest.js
Normal file
61
app/extensions/registry-management/rest/manifest.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
angular.module('portainer.extensions.registrymanagement')
|
||||
.factory('RegistryManifests', ['$resource', 'API_ENDPOINT_REGISTRIES', function RegistryManifestsFactory($resource, API_ENDPOINT_REGISTRIES) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_REGISTRIES + '/:id/v2/:repository/manifests/:tag', {}, {
|
||||
get: {
|
||||
method: 'GET',
|
||||
params: {
|
||||
id: '@id',
|
||||
repository: '@repository',
|
||||
tag: '@tag'
|
||||
},
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache'
|
||||
},
|
||||
transformResponse: function (data, headers) {
|
||||
var response = angular.fromJson(data);
|
||||
response.digest = headers('docker-content-digest');
|
||||
return response;
|
||||
}
|
||||
},
|
||||
getV2: {
|
||||
method: 'GET',
|
||||
params: {
|
||||
id: '@id',
|
||||
repository: '@repository',
|
||||
tag: '@tag'
|
||||
},
|
||||
headers: {
|
||||
'Accept': 'application/vnd.docker.distribution.manifest.v2+json',
|
||||
'Cache-Control': 'no-cache'
|
||||
},
|
||||
transformResponse: function (data, headers) {
|
||||
var response = angular.fromJson(data);
|
||||
response.digest = headers('docker-content-digest');
|
||||
return response;
|
||||
}
|
||||
},
|
||||
put: {
|
||||
method: 'PUT',
|
||||
params: {
|
||||
id: '@id',
|
||||
repository: '@repository',
|
||||
tag: '@tag'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json'
|
||||
},
|
||||
transformRequest: function (data) {
|
||||
return angular.toJson(data, 3);
|
||||
}
|
||||
},
|
||||
delete: {
|
||||
method: 'DELETE',
|
||||
params: {
|
||||
id: '@id',
|
||||
repository: '@repository',
|
||||
tag: '@tag'
|
||||
}
|
||||
}
|
||||
});
|
||||
}]);
|
10
app/extensions/registry-management/rest/tags.js
Normal file
10
app/extensions/registry-management/rest/tags.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
angular.module('portainer.extensions.registrymanagement')
|
||||
.factory('RegistryTags', ['$resource', 'API_ENDPOINT_REGISTRIES', function RegistryTagsFactory($resource, API_ENDPOINT_REGISTRIES) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_REGISTRIES + '/:id/v2/:repository/tags/list', {}, {
|
||||
get: {
|
||||
method: 'GET',
|
||||
params: { id: '@id', repository: '@repository' }
|
||||
}
|
||||
});
|
||||
}]);
|
|
@ -0,0 +1,118 @@
|
|||
angular.module('portainer.extensions.registrymanagement')
|
||||
.factory('RegistryV2Service', ['$q', 'RegistryCatalog', 'RegistryTags', 'RegistryManifests', 'RegistryV2Helper',
|
||||
function RegistryV2ServiceFactory($q, RegistryCatalog, RegistryTags, RegistryManifests, RegistryV2Helper) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.ping = function(id, forceNewConfig) {
|
||||
if (forceNewConfig) {
|
||||
return RegistryCatalog.pingWithForceNew({ id: id }).$promise;
|
||||
}
|
||||
return RegistryCatalog.ping({ id: id }).$promise;
|
||||
};
|
||||
|
||||
service.repositories = function (id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
RegistryCatalog.get({
|
||||
id: id
|
||||
}).$promise
|
||||
.then(function success(data) {
|
||||
var promises = [];
|
||||
for (var i = 0; i < data.repositories.length; i++) {
|
||||
var repository = data.repositories[i];
|
||||
promises.push(RegistryTags.get({
|
||||
id: id,
|
||||
repository: repository
|
||||
}).$promise);
|
||||
}
|
||||
return $q.all(promises);
|
||||
})
|
||||
.then(function success(data) {
|
||||
var repositories = data.map(function (item) {
|
||||
if (!item.tags) {
|
||||
return;
|
||||
}
|
||||
return new RegistryRepositoryViewModel(item);
|
||||
});
|
||||
repositories = _.without(repositories, undefined);
|
||||
deferred.resolve(repositories);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({
|
||||
msg: 'Unable to retrieve repositories',
|
||||
err: err
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.tags = function (id, repository) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
RegistryTags.get({
|
||||
id: id,
|
||||
repository: repository
|
||||
}).$promise
|
||||
.then(function succes(data) {
|
||||
deferred.resolve(data.tags);
|
||||
}).catch(function error(err) {
|
||||
deferred.reject({
|
||||
msg: 'Unable to retrieve tags',
|
||||
err: err
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.tag = function (id, repository, tag) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
var promises = {
|
||||
v1: RegistryManifests.get({
|
||||
id: id,
|
||||
repository: repository,
|
||||
tag: tag
|
||||
}).$promise,
|
||||
v2: RegistryManifests.getV2({
|
||||
id: id,
|
||||
repository: repository,
|
||||
tag: tag
|
||||
}).$promise
|
||||
};
|
||||
$q.all(promises)
|
||||
.then(function success(data) {
|
||||
var tag = RegistryV2Helper.manifestsToTag(data);
|
||||
deferred.resolve(tag);
|
||||
}).catch(function error(err) {
|
||||
deferred.reject({
|
||||
msg: 'Unable to retrieve tag ' + tag,
|
||||
err: err
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.addTag = function (id, repository, tag, manifest) {
|
||||
delete manifest.digest;
|
||||
return RegistryManifests.put({
|
||||
id: id,
|
||||
repository: repository,
|
||||
tag: tag
|
||||
}, manifest).$promise;
|
||||
};
|
||||
|
||||
service.deleteManifest = function (id, repository, digest) {
|
||||
return RegistryManifests.delete({
|
||||
id: id,
|
||||
repository: repository,
|
||||
tag: digest
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
]);
|
|
@ -0,0 +1,66 @@
|
|||
angular.module('portainer.extensions.registrymanagement')
|
||||
.controller('ConfigureRegistryController', ['$scope', '$state', '$transition$', 'RegistryService', 'RegistryV2Service', 'Notifications',
|
||||
function ($scope, $state, $transition$, RegistryService, RegistryV2Service, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
testInProgress: false,
|
||||
updateInProgress: false,
|
||||
validConfiguration : false
|
||||
};
|
||||
|
||||
$scope.testConfiguration = testConfiguration;
|
||||
$scope.updateConfiguration = updateConfiguration;
|
||||
|
||||
function testConfiguration() {
|
||||
$scope.state.testInProgress = true;
|
||||
|
||||
RegistryService.configureRegistry($scope.registry.Id, $scope.model)
|
||||
.then(function success() {
|
||||
return RegistryV2Service.ping($scope.registry.Id, true);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Success', 'Valid management configuration');
|
||||
$scope.state.validConfiguration = true;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Invalid management configuration');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.testInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
function updateConfiguration() {
|
||||
$scope.state.updateInProgress = true;
|
||||
|
||||
RegistryService.configureRegistry($scope.registry.Id, $scope.model)
|
||||
.then(function success() {
|
||||
Notifications.success('Success', 'Registry management configuration updated');
|
||||
$state.go('portainer.registries.registry.repositories', { id: $scope.registry.Id }, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update registry management configuration');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.updateInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
var registryId = $transition$.params().id;
|
||||
|
||||
RegistryService.registry(registryId)
|
||||
.then(function success(data) {
|
||||
var registry = data;
|
||||
var model = new RegistryManagementConfigurationDefaultModel(registry);
|
||||
|
||||
$scope.registry = registry;
|
||||
$scope.model = model;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve registry details');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -0,0 +1,161 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Configure registry"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.registries">Registries</a> > <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a> > Management configuration
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Information
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
The following configuration will be used to access this <a href="https://docs.docker.com/registry/spec/api/" target="_blank">registry API</a> to provide Portainer management features.
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Registry details
|
||||
</div>
|
||||
<!-- registry-url-input -->
|
||||
<div class="form-group">
|
||||
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Registry URL
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="registry_url" ng-model="registry.URL" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !registry-url-input -->
|
||||
<!-- authentication-checkbox -->
|
||||
<div class="form-group" ng-if="registry.Type === 3">
|
||||
<div class="col-sm-12">
|
||||
<label for="registry_auth" class="control-label text-left">
|
||||
Authentication
|
||||
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to this registry."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="model.Authentication"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !authentication-checkbox -->
|
||||
<!-- authentication-credentials -->
|
||||
<div ng-if="model.Authentication">
|
||||
<!-- credentials-user -->
|
||||
<div class="form-group">
|
||||
<label for="credentials_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="credentials_username" ng-model="model.Username">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !credentials-user -->
|
||||
<!-- credentials-password -->
|
||||
<div class="form-group">
|
||||
<label for="credentials_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="password" class="form-control" id="credentials_password" ng-model="model.Password" placeholder="*******">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !credentials-password -->
|
||||
</div>
|
||||
<!-- !authentication-credentials -->
|
||||
<!-- tls -->
|
||||
<div ng-if="registry.Type === 3">
|
||||
<!-- tls-checkbox -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="tls" class="control-label text-left">
|
||||
TLS
|
||||
<portainer-tooltip position="bottom" message="Enable this option if you need to connect to the registry API with TLS."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="model.TLS"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-checkbox -->
|
||||
<!-- tls-skip-verify -->
|
||||
<div class="form-group" ng-if="model.TLS">
|
||||
<div class="col-sm-12">
|
||||
<label for="tls" class="control-label text-left">
|
||||
Skip certificate verification
|
||||
<portainer-tooltip position="bottom" message="Skip the verification of the server TLS certificate. Not recommended on unsecured networks."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="model.TLSSkipVerify"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-skip-verify -->
|
||||
<div class="col-sm-12 form-section-title" ng-if="model.TLS && !model.TLSSkipVerify">
|
||||
Required TLS files
|
||||
</div>
|
||||
<!-- tls-file-upload -->
|
||||
<div ng-if="model.TLS && !model.TLSSkipVerify">
|
||||
<!-- tls-ca-file-cert -->
|
||||
<div class="form-group">
|
||||
<label for="tls_cert" class="col-sm-3 col-lg-2 control-label text-left">TLS CA certificate</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="model.TLSCACertFile">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ model.TLSCACertFile.name }}
|
||||
<i class="fa fa-check green-icon" ng-if="model.TLSCACertFile && model.TLSCACertFile === registry.ManagementConfiguration.TLSConfig.TLSCACertFile" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!model.TLSCACertFile" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-ca-file-cert -->
|
||||
<!-- tls-file-cert -->
|
||||
<div class="form-group">
|
||||
<label for="tls_cert" class="col-sm-3 col-lg-2 control-label text-left">TLS certificate</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="model.TLSCertFile">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ model.TLSCertFile.name }}
|
||||
<i class="fa fa-check green-icon" ng-if="model.TLSCertFile && model.TLSCertFile === registry.ManagementConfiguration.TLSConfig.TLSCertFile" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!model.TLSCertFile" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-file-cert -->
|
||||
<!-- tls-file-key -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left">TLS key</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="model.TLSKeyFile">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ model.TLSKeyFile.name }}
|
||||
<i class="fa fa-check green-icon" ng-if="model.TLSKeyFile && model.TLSKeyFile === registry.ManagementConfiguration.TLSConfig.TLSKeyFile" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!model.TLSKeyFile" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-file-key -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.testInProgress" ng-click="testConfiguration()" button-spinner="state.testInProgress">
|
||||
<span ng-hide="state.testInProgress">Test configuration</span>
|
||||
<span ng-show="state.testInProgress">Test in progress...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.updateInProgress || !state.validConfiguration" ng-click="updateConfiguration()" button-spinner="state.updateInProgress">
|
||||
<span ng-hide="state.updateInProgress">Update configuration</span>
|
||||
<span ng-show="state.updateInProgress">Updating configuration...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,88 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Repository">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.registries.registry.repository" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.registries">Registries</a> >
|
||||
<a ui-sref="portainer.registries.registry.repositories({id: registry.Id})">{{ registry.Name }}</a> >
|
||||
<a ui-sref="portainer.registries.registry.repository()">{{ repository.Name }} </a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-info" title-text="Repository information">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Repository</td>
|
||||
<td>
|
||||
{{ repository.Name }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeRepository()">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this repository
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tags count</td>
|
||||
<td>{{ repository.Tags.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Images count</td>
|
||||
<td>{{ repository.Images.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plus" title-text="Add tag">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label for="tag" class="col-sm-3 col-lg-2 control-label text-left">Tag</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="tag" ng-model="formValues.Tag">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="image" class="col-sm-3 col-lg-2 control-label text-left">Image</label>
|
||||
<ui-select class="col-sm-9 col-lg-10" ng-model="formValues.SelectedImage" id="image">
|
||||
<ui-select-match placeholder="Select an image" allow-clear="true">
|
||||
<span>{{ $select.selected }}</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="image in (repository.Images | filter: $select.search)">
|
||||
<span>{{ image }}</span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.Tag || !formValues.SelectedImage"
|
||||
ng-click="addTag()" button-spinner="state.actionInProgress">
|
||||
<span ng-hide="state.actionInProgress">Add tag</span>
|
||||
<span ng-show="state.actionInProgress">Adding tag...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<registries-repository-tags-datatable title-text="Tags" title-icon="fa-tags" dataset="tags" table-key="registryRepositoryTags"
|
||||
order-by="Name" remove-action="removeTags" retag-action="retagAction"></registries-repository-tags-datatable>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,150 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('RegistryRepositoryController', ['$q', '$scope', '$transition$', '$state', 'RegistryV2Service', 'RegistryService', 'ModalService', 'Notifications',
|
||||
function ($q, $scope, $transition$, $state, RegistryV2Service, RegistryService, ModalService, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
$scope.formValues = {
|
||||
Tag: ''
|
||||
};
|
||||
$scope.tags = [];
|
||||
$scope.repository = {
|
||||
Name: [],
|
||||
Tags: [],
|
||||
Images: []
|
||||
};
|
||||
|
||||
$scope.$watch('tags.length', function () {
|
||||
var images = $scope.tags.map(function (item) {
|
||||
return item.ImageId;
|
||||
});
|
||||
$scope.repository.Images = _.uniq(images);
|
||||
});
|
||||
|
||||
$scope.addTag = function () {
|
||||
var manifest = $scope.tags.find(function (item) {
|
||||
return item.ImageId === $scope.formValues.SelectedImage;
|
||||
}).ManifestV2;
|
||||
RegistryV2Service.addTag($scope.registryId, $scope.repository.Name, $scope.formValues.Tag, manifest)
|
||||
.then(function success() {
|
||||
Notifications.success('Success', 'Tag successfully added');
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to add tag');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.retagAction = function (tag) {
|
||||
RegistryV2Service.deleteManifest($scope.registryId, $scope.repository.Name, tag.Digest)
|
||||
.then(function success() {
|
||||
var promises = [];
|
||||
var tagsToAdd = $scope.tags.filter(function (item) {
|
||||
return item.Digest === tag.Digest;
|
||||
});
|
||||
tagsToAdd.map(function (item) {
|
||||
var tagValue = item.Modified && item.Name !== item.NewName ? item.NewName : item.Name;
|
||||
promises.push(RegistryV2Service.addTag($scope.registryId, $scope.repository.Name, tagValue, item.ManifestV2));
|
||||
});
|
||||
return $q.all(promises);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Success', 'Tag successfully modified');
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to modify tag');
|
||||
tag.Modified = false;
|
||||
tag.NewValue = tag.Value;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeTags = function (selectedItems) {
|
||||
ModalService.confirmDeletion(
|
||||
'Are you sure you want to remove the selected tags ?',
|
||||
function onConfirm(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
var promises = [];
|
||||
var uniqItems = _.uniqBy(selectedItems, 'Digest');
|
||||
uniqItems.map(function (item) {
|
||||
promises.push(RegistryV2Service.deleteManifest($scope.registryId, $scope.repository.Name, item.Digest));
|
||||
});
|
||||
$q.all(promises)
|
||||
.then(function success() {
|
||||
var promises = [];
|
||||
var tagsToReupload = _.differenceBy($scope.tags, selectedItems, 'Name');
|
||||
tagsToReupload.map(function (item) {
|
||||
promises.push(RegistryV2Service.addTag($scope.registryId, $scope.repository.Name, item.Name, item.ManifestV2));
|
||||
});
|
||||
return $q.all(promises);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Success', 'Tags successfully deleted');
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to delete tags');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeRepository = function () {
|
||||
ModalService.confirmDeletion(
|
||||
'This action will only remove the manifests linked to this repository. You need to manually trigger a garbage collector pass on your registry to remove orphan layers and really remove the images content. THIS ACTION CAN NOT BE UNDONE',
|
||||
function onConfirm(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
var promises = [];
|
||||
var uniqItems = _.uniqBy($scope.tags, 'Digest');
|
||||
uniqItems.map(function (item) {
|
||||
promises.push(RegistryV2Service.deleteManifest($scope.registryId, $scope.repository.Name, item.Digest));
|
||||
});
|
||||
$q.all(promises)
|
||||
.then(function success() {
|
||||
Notifications.success('Success', 'Repository sucessfully removed');
|
||||
$state.go('portainer.registries.registry.repositories', {
|
||||
id: $scope.registryId
|
||||
}, {
|
||||
reload: true
|
||||
});
|
||||
}).catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to delete repository');
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function initView() {
|
||||
var registryId = $scope.registryId = $transition$.params().id;
|
||||
var repository = $scope.repository.Name = $transition$.params().repository;
|
||||
$q.all({
|
||||
registry: RegistryService.registry(registryId),
|
||||
tags: RegistryV2Service.tags(registryId, repository)
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.registry = data.registry;
|
||||
$scope.repository.Tags = data.tags;
|
||||
$scope.tags = [];
|
||||
for (var i = 0; i < data.tags.length; i++) {
|
||||
var tag = data.tags[i];
|
||||
RegistryV2Service.tag(registryId, repository, tag)
|
||||
.then(function success(data) {
|
||||
$scope.tags.push(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve tag information');
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve repository information');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}
|
||||
]);
|
|
@ -0,0 +1,37 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Registry repositories">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.registries.registry.repositories" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.registries">Registries</a> > <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a> > Repositories
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<information-panel ng-if="state.displayInvalidConfigurationMessage" title-text="Registry management configuration required">
|
||||
<span class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Portainer was not able to use this registry management features. You might need to update the configuration used by Portainer to access this registry.
|
||||
</p>
|
||||
<p>Note: Portainer registry management features are only supported with registries exposing the <a href="https://docs.docker.com/registry/spec/api/" target="_blank">v2 registry API</a>.</p>
|
||||
<div style="margin-top: 7px;">
|
||||
<a ui-sref="portainer.registries.registry.configure({id: registry.Id})">
|
||||
<i class="fa fa-wrench" aria-hidden="true"></i> Configure this registry
|
||||
</a>
|
||||
</div>
|
||||
</span>
|
||||
</information-panel>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="repositories">
|
||||
<div class="col-sm-12">
|
||||
<registry-repositories-datatable
|
||||
title-text="Repositories" title-icon="fa-book"
|
||||
dataset="repositories" table-key="registryRepositories"
|
||||
order-by="Name">
|
||||
</registry-repositories-datatable>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,33 @@
|
|||
angular.module('portainer.extensions.registrymanagement')
|
||||
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications',
|
||||
function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
displayInvalidConfigurationMessage: false
|
||||
};
|
||||
|
||||
function initView() {
|
||||
var registryId = $transition$.params().id;
|
||||
|
||||
RegistryService.registry(registryId)
|
||||
.then(function success(data) {
|
||||
$scope.registry = data;
|
||||
|
||||
RegistryV2Service.ping(registryId, false)
|
||||
.then(function success() {
|
||||
return RegistryV2Service.repositories(registryId);
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.repositories = data;
|
||||
})
|
||||
.catch(function error() {
|
||||
$scope.state.displayInvalidConfigurationMessage = true;
|
||||
});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve registry details');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -1,3 +1,4 @@
|
|||
// TODO: legacy extension management
|
||||
angular.module('extension.storidge', [])
|
||||
.config(['$stateRegistryProvider', function ($stateRegistryProvider) {
|
||||
'use strict';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue