mirror of https://github.com/portainer/portainer
fix(registry): Performance issues with Registry Manager (#2648)
* fix(registry): fetch datatable details on page/filter/order state change instead of fetching all data on first load * fix(registry): fetch tags datatable details on state change instead of fetching all data on first load * fix(registry): add pagination support for tags + loading display on data load * fix(registry): debounce on text filter to avoid querying transient matching values * refactor(registry): rebase on latest develop * feat(registries): background tags and optimisation -- need code cleanup and for-await-of to cancel on page leave * refactor(registry-management): code cleanup * feat(registry): most optimized version -- need fix for add/retag * fix(registry): addTag working without page reload * fix(registry): retag working without reload * fix(registry): remove tag working without reload * fix(registry): remove repository working with latest changes * fix(registry): disable cache on firefox * feat(registry): use jquery for all 'most used' manifests requests * feat(registry): retag with progression + rewrite manifest REST service to jquery * fix(registry): remove forgotten DI * fix(registry): pagination on repository details * refactor(registry): info message + hidding images count until fetch has been done * fix(registry): fix selection reset deleting selectAll function and not resetting status * fix(registry): resetSelection was trying to set value on a getter * fix(registry): tags were dropped when too much tags were impacted by a tag removal * fix(registry): firefox add tag + progression * refactor(registry): rewording of elements * style(registry): add space between buttons and texts in status elements * fix(registry): cancelling a retag/delete action was not removing the status panel * fix(registry): tags count of empty repositories * feat(registry): reload page on action cancel to avoid desync * feat(registry): uncancellable modal on long operations * feat(registry): modal now closes on error + modal message improvement * feat(registries): remove empty repositories from the list * fix(registry): various bugfixes * feat(registry): independant timer on async actions + modal fixpull/3292/head
parent
8a8cef9b20
commit
2445a5aed5
2
.babelrc
2
.babelrc
|
@ -5,7 +5,7 @@
|
|||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false,
|
||||
"useBuiltIns": "usage"
|
||||
"useBuiltIns": "entry"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -24,7 +24,7 @@ rules:
|
|||
# no-cond-assign: error
|
||||
# no-console: off
|
||||
# no-constant-condition: error
|
||||
# no-control-regex: error
|
||||
no-control-regex: off
|
||||
# no-debugger: error
|
||||
# no-dupe-args: error
|
||||
# no-dupe-keys: error
|
||||
|
|
14
app/app.js
14
app/app.js
|
@ -1,8 +1,10 @@
|
|||
import _ from 'lodash-es';
|
||||
import $ from 'jquery';
|
||||
import '@babel/polyfill'
|
||||
|
||||
angular.module('portainer')
|
||||
.run(['$rootScope', '$state', '$interval', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
||||
function ($rootScope, $state, $interval, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
||||
.run(['$rootScope', '$state', '$interval', 'LocalStorage', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
||||
function ($rootScope, $state, $interval, LocalStorage, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
||||
'use strict';
|
||||
|
||||
EndpointProvider.initialize();
|
||||
|
@ -40,6 +42,14 @@ function ($rootScope, $state, $interval, Authentication, authManager, StateManag
|
|||
ping(EndpointProvider, SystemService);
|
||||
}, 60 * 1000)
|
||||
|
||||
$(document).ajaxSend(function (event, jqXhr, jqOpts) {
|
||||
const type = jqOpts.type === 'POST' || jqOpts.type === 'PUT' || jqOpts.type === 'PATCH';
|
||||
const hasNoContentType = jqOpts.contentType !== 'application/json' && jqOpts.headers && !jqOpts.headers['Content-Type'];
|
||||
if (type && hasNoContentType) {
|
||||
jqXhr.setRequestHeader('Content-Type', 'application/json');
|
||||
}
|
||||
jqXhr.setRequestHeader('Authorization', 'Bearer ' + LocalStorage.getJWT());
|
||||
});
|
||||
}]);
|
||||
|
||||
function ping(EndpointProvider, SystemService) {
|
||||
|
|
|
@ -278,6 +278,9 @@ angular.module('portainer.docker')
|
|||
.filter('trimshasum', function () {
|
||||
'use strict';
|
||||
return function (imageName) {
|
||||
if (!imageName) {
|
||||
return;
|
||||
}
|
||||
if (imageName.indexOf('sha256:') === 0) {
|
||||
return imageName.substring(7, 19);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,12 @@ angular.module('portainer.docker')
|
|||
|
||||
var helper = {};
|
||||
|
||||
helper.isValidTag = isValidTag;
|
||||
|
||||
function isValidTag(tag) {
|
||||
return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g);
|
||||
}
|
||||
|
||||
helper.extractImageAndRegistryFromRepository = function(repository) {
|
||||
var slashCount = _.countBy(repository)['/'];
|
||||
var registry = null;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</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 ng-model-options="{ debounce: 300 }">
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-model-options="{ debounce: 300 }" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-filters nowrap-cells">
|
||||
|
@ -22,16 +22,12 @@
|
|||
</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))"
|
||||
<tr ng-hide="$ctrl.loading" 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>
|
||||
<a ui-sref="portainer.registries.registry.repository({repository: item.Name})" class="monospaced"
|
||||
|
@ -39,7 +35,7 @@
|
|||
</td>
|
||||
<td>{{ item.TagsCount }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<tr ng-if="!$ctrl.dataset || $ctrl.loading">
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
|
@ -59,7 +55,6 @@
|
|||
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>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.extensions.registrymanagement').component('registryRepositoriesDatatable', {
|
||||
templateUrl: './registryRepositoriesDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
controller: 'RegistryRepositoriesDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
|
@ -8,6 +8,7 @@ angular.module('portainer.extensions.registrymanagement').component('registryRep
|
|||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<'
|
||||
paginationAction: '<',
|
||||
loading: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.controller('RegistryRepositoriesDatatableController', ['$scope', '$controller',
|
||||
function($scope, $controller) {
|
||||
var ctrl = this;
|
||||
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
this.state.orderBy = this.orderBy;
|
||||
|
||||
function areDifferent(a, b) {
|
||||
if (!a || !b) {
|
||||
return true;
|
||||
}
|
||||
var namesA = a.map( function(x){ return x.Name; } ).sort();
|
||||
var namesB = b.map( function(x){ return x.Name; } ).sort();
|
||||
return namesA.join(',') !== namesB.join(',');
|
||||
}
|
||||
|
||||
$scope.$watch(function() { return ctrl.state.filteredDataSet;},
|
||||
function(newValue, oldValue) {
|
||||
if (newValue && areDifferent(oldValue, newValue)) {
|
||||
ctrl.paginationAction(_.filter(newValue, {'TagsCount':0}));
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
]);
|
|
@ -3,18 +3,17 @@
|
|||
<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 }}
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<div class="actionBar" ng-if="$ctrl.advancedFeaturesAvailable">
|
||||
<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 ng-model-options="{ debounce: 300 }">
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-model-options="{ debounce: 300 }" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
|
@ -32,25 +31,13 @@
|
|||
</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>
|
||||
<th>Image ID</th>
|
||||
<th>Compressed size</th>
|
||||
<th ng-if="$ctrl.advancedFeaturesAvailable">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))"
|
||||
<tr ng-hide="$ctrl.loading" 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">
|
||||
|
@ -60,15 +47,16 @@
|
|||
{{ item.Name }}
|
||||
</td>
|
||||
<td>{{ item.Os }}/{{ item.Architecture }}</td>
|
||||
<td>{{ item.ImageId | truncate:40 }}</td>
|
||||
<td>{{ item.ImageId | trimshasum }}</td>
|
||||
<td>{{ item.Size | humansize }}</td>
|
||||
<td>
|
||||
<td ng-if="$ctrl.advancedFeaturesAvailable">
|
||||
<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">
|
||||
<portainer-tooltip position="bottom" message="Tag can only contain alphanumeric (a-zA-Z0-9) and special _ . - characters. Tag must not start with . - characters."></portainer-tooltip>
|
||||
<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>
|
||||
|
@ -76,11 +64,11 @@
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="3" class="text-center text-muted">Loading...</td>
|
||||
<tr ng-if="$ctrl.loading">
|
||||
<td colspan="5" 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 ng-if="!$ctrl.loading && $ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="5" class="text-center text-muted">No tag available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -96,7 +84,6 @@
|
|||
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>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.extensions.registrymanagement').component('registriesRepositoryTagsDatatable', {
|
||||
templateUrl: './registriesRepositoryTagsDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
controller: 'RegistryRepositoriesTagsDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
|
@ -9,6 +9,9 @@ angular.module('portainer.extensions.registrymanagement').component('registriesR
|
|||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
retagAction: '<'
|
||||
retagAction: '<',
|
||||
advancedFeaturesAvailable: '<',
|
||||
paginationAction: '<',
|
||||
loading: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.controller('RegistryRepositoriesTagsDatatableController', ['$scope', '$controller',
|
||||
function($scope, $controller) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
var ctrl = this;
|
||||
this.state.orderBy = this.orderBy;
|
||||
|
||||
function diff(item) {
|
||||
return item.Name + item.ImageDigest;
|
||||
}
|
||||
|
||||
function areDifferent(a, b) {
|
||||
if (!a || !b) {
|
||||
return true;
|
||||
}
|
||||
var namesA = _.sortBy(_.map(a, diff));
|
||||
var namesB = _.sortBy(_.map(b, diff));
|
||||
return namesA.join(',') !== namesB.join(',');
|
||||
}
|
||||
|
||||
$scope.$watch(function() { return ctrl.state.filteredDataSet;},
|
||||
function(newValue, oldValue) {
|
||||
if (newValue && newValue.length && areDifferent(oldValue, newValue)) {
|
||||
ctrl.paginationAction(_.filter(newValue, {'ImageId': ''}));
|
||||
ctrl.resetSelectionState();
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
]);
|
|
@ -7,12 +7,7 @@ angular.module('portainer.extensions.registrymanagement')
|
|||
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;
|
||||
return angular.fromJson(rawHistory[0].v1Compatibility);
|
||||
}
|
||||
|
||||
helper.manifestsToTag = function (manifests) {
|
||||
|
@ -20,20 +15,18 @@ angular.module('portainer.extensions.registrymanagement')
|
|||
var v2 = manifests.v2;
|
||||
|
||||
var history = historyRawToParsed(v1.history);
|
||||
var imageId = history[0].id;
|
||||
var name = v1.tag;
|
||||
var os = history[0].os;
|
||||
var os = history.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;
|
||||
var imageId = v2.config.digest;
|
||||
var imageDigest = v2.digest;
|
||||
|
||||
return new RepositoryTagViewModel(name, imageId, os, arch, size, digest, repositoryName, fsLayers, history, v2);
|
||||
return new RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2);
|
||||
};
|
||||
|
||||
return helper;
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
export function RegistryRepositoryViewModel(data) {
|
||||
this.Name = data.name;
|
||||
this.TagsCount = data.tags.length;
|
||||
import _ from 'lodash-es';
|
||||
export default function RegistryRepositoryViewModel(item) {
|
||||
if (item.name && item.tags) {
|
||||
this.Name = item.name;
|
||||
this.TagsCount = _.without(item.tags, null).length;
|
||||
} else {
|
||||
this.Name = item;
|
||||
this.TagsCount = 0;
|
||||
}
|
||||
}
|
|
@ -1,12 +1,16 @@
|
|||
export function RepositoryTagViewModel(name, imageId, os, arch, size, digest, repositoryName, fsLayers, history, manifestv2) {
|
||||
export function RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2) {
|
||||
this.Name = name;
|
||||
this.Os = os || '';
|
||||
this.Architecture = arch || '';
|
||||
this.Size = size || 0;
|
||||
this.ImageDigest = imageDigest || '';
|
||||
this.ImageId = imageId || '';
|
||||
this.ManifestV2 = v2 || {};
|
||||
}
|
||||
|
||||
export function RepositoryShortTag(name, imageId, imageDigest, manifest) {
|
||||
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;
|
||||
this.ImageDigest = imageDigest;
|
||||
this.ManifestV2 = manifest;
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
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'
|
||||
}
|
||||
}
|
||||
});
|
||||
}]);
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* This service has been created to request the docker registry API
|
||||
* without triggering AngularJS digest cycles
|
||||
* For more information, see https://github.com/portainer/portainer/pull/2648#issuecomment-505644913
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
angular.module('portainer.extensions.registrymanagement')
|
||||
.factory('RegistryManifestsJquery', ['API_ENDPOINT_REGISTRIES',
|
||||
function RegistryManifestsJqueryFactory(API_ENDPOINT_REGISTRIES) {
|
||||
'use strict';
|
||||
|
||||
function buildUrl(params) {
|
||||
return API_ENDPOINT_REGISTRIES + '/' + params.id + '/v2/' + params.repository + '/manifests/'+ params.tag;
|
||||
}
|
||||
|
||||
function _get(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
dataType: 'JSON',
|
||||
url: buildUrl(params),
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'If-Modified-Since':'Mon, 26 Jul 1997 05:00:00 GMT'
|
||||
},
|
||||
success: (result) => resolve(result),
|
||||
error: (error) => reject(error)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function _getV2(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
dataType: 'JSON',
|
||||
url: buildUrl(params),
|
||||
headers: {
|
||||
'Accept': 'application/vnd.docker.distribution.manifest.v2+json',
|
||||
'Cache-Control': 'no-cache',
|
||||
'If-Modified-Since':'Mon, 26 Jul 1997 05:00:00 GMT'
|
||||
},
|
||||
success: (result, status, request) => {
|
||||
result.digest = request.getResponseHeader('Docker-Content-Digest');
|
||||
resolve(result);
|
||||
},
|
||||
error: (error) => reject(error)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function _put(params, data) {
|
||||
const transformRequest = (d) => {
|
||||
return angular.toJson(d, 3);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
type: 'PUT',
|
||||
url: buildUrl(params),
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json'
|
||||
},
|
||||
data: transformRequest(data),
|
||||
success: (result) => resolve(result),
|
||||
error: (error) => reject(error)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function _delete(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
url: buildUrl(params),
|
||||
success: (result) => resolve(result),
|
||||
error: (error) => reject(error)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
get: _get,
|
||||
getV2: _getV2,
|
||||
put: _put,
|
||||
delete: _delete
|
||||
}
|
||||
}]);
|
|
@ -1,10 +1,13 @@
|
|||
import linkGetResponse from './transform/linkGetResponse';
|
||||
|
||||
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' }
|
||||
params: { id: '@id', repository: '@repository' },
|
||||
transformResponse: linkGetResponse
|
||||
}
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
function findBestStep(length) {
|
||||
let step = Math.trunc(length / 10);
|
||||
if (step < 10) {
|
||||
step = 10;
|
||||
} else if (step > 100) {
|
||||
step = 100;
|
||||
}
|
||||
return step;
|
||||
}
|
||||
|
||||
export default async function* genericAsyncGenerator($q, list, func, params) {
|
||||
const step = findBestStep(list.length);
|
||||
let start = 0;
|
||||
let end = start + step;
|
||||
let results = [];
|
||||
while (start < list.length) {
|
||||
const batch = _.slice(list, start, end);
|
||||
const promises = [];
|
||||
for (let i = 0; i < batch.length; i++) {
|
||||
promises.push(func(...params, batch[i]));
|
||||
}
|
||||
yield start;
|
||||
const res = await $q.all(promises);
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
results.push(res[i]);
|
||||
}
|
||||
start = end;
|
||||
end += step;
|
||||
}
|
||||
yield list.length;
|
||||
yield results;
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
import { RegistryRepositoryViewModel } from '../models/registryRepository';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
function getCatalog(id) {
|
||||
var deferred = $q.defer();
|
||||
var repositories = [];
|
||||
|
||||
_getCatalogPage({id: id}, deferred, repositories);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function _getCatalogPage(params, deferred, repositories) {
|
||||
RegistryCatalog.get(params).$promise.then(function(data) {
|
||||
repositories = _.concat(repositories, data.repositories);
|
||||
if (data.last && data.n) {
|
||||
_getCatalogPage({id: params.id, n: data.n, last: data.last}, deferred, repositories);
|
||||
} else {
|
||||
deferred.resolve(repositories);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
service.repositories = function (id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
getCatalog(id).then(function success(data) {
|
||||
var promises = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var repository = data[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,214 @@
|
|||
import _ from 'lodash-es';
|
||||
import { RepositoryShortTag } from '../models/repositoryTag';
|
||||
import RegistryRepositoryViewModel from '../models/registryRepository';
|
||||
import genericAsyncGenerator from './genericAsyncGenerator';
|
||||
|
||||
angular.module('portainer.extensions.registrymanagement')
|
||||
.factory('RegistryV2Service', ['$q', '$async', 'RegistryCatalog', 'RegistryTags', 'RegistryManifestsJquery', 'RegistryV2Helper',
|
||||
function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, RegistryManifestsJquery, RegistryV2Helper) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.ping = function(id, forceNewConfig) {
|
||||
if (forceNewConfig) {
|
||||
return RegistryCatalog.pingWithForceNew({ id: id }).$promise;
|
||||
}
|
||||
return RegistryCatalog.ping({ id: id }).$promise;
|
||||
};
|
||||
|
||||
function _getCatalogPage(params, deferred, repositories) {
|
||||
RegistryCatalog.get(params).$promise.then(function(data) {
|
||||
repositories = _.concat(repositories, data.repositories);
|
||||
if (data.last && data.n) {
|
||||
_getCatalogPage({id: params.id, n: data.n, last: data.last}, deferred, repositories);
|
||||
} else {
|
||||
deferred.resolve(repositories);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCatalog(id) {
|
||||
var deferred = $q.defer();
|
||||
var repositories = [];
|
||||
|
||||
_getCatalogPage({id: id}, deferred, repositories);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
service.catalog = function (id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
getCatalog(id).then(function success(data) {
|
||||
var repositories = data.map(function (repositoryName) {
|
||||
return new RegistryRepositoryViewModel(repositoryName);
|
||||
});
|
||||
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();
|
||||
|
||||
_getTagsPage({id: id, repository: repository}, deferred, {tags:[]});
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
function _getTagsPage(params, deferred, previousTags) {
|
||||
RegistryTags.get(params).$promise.then(function(data) {
|
||||
previousTags.name = data.name;
|
||||
previousTags.tags = _.concat(previousTags.tags, data.tags);
|
||||
if (data.last && data.n) {
|
||||
_getTagsPage({id: params.id, repository: params.repository, n: data.n, last: data.last}, deferred, previousTags);
|
||||
} else {
|
||||
deferred.resolve(previousTags);
|
||||
}
|
||||
}).catch(function error(err) {
|
||||
deferred.reject({
|
||||
msg: 'Unable to retrieve tags',
|
||||
err: err
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
service.getRepositoriesDetails = function (id, repositories) {
|
||||
var deferred = $q.defer();
|
||||
var promises = [];
|
||||
for (var i = 0; i < repositories.length; i++) {
|
||||
var repository = repositories[i].Name;
|
||||
promises.push(service.tags(id, repository));
|
||||
}
|
||||
|
||||
$q.all(promises)
|
||||
.then(function success(data) {
|
||||
var repositories = data.map(function (item) {
|
||||
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.getTagsDetails = function (id, repository, tags) {
|
||||
var promises = [];
|
||||
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
var tag = tags[i].Name;
|
||||
promises.push(service.tag(id, repository, tag));
|
||||
}
|
||||
|
||||
return $q.all(promises);
|
||||
};
|
||||
|
||||
service.tag = function (id, repository, tag) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
var promises = {
|
||||
v1: RegistryManifestsJquery.get({
|
||||
id: id,
|
||||
repository: repository,
|
||||
tag: tag
|
||||
}),
|
||||
v2: RegistryManifestsJquery.getV2({
|
||||
id: id,
|
||||
repository: repository,
|
||||
tag: tag
|
||||
})
|
||||
};
|
||||
$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 RegistryManifestsJquery.put({
|
||||
id: id,
|
||||
repository: repository,
|
||||
tag: tag
|
||||
}, manifest);
|
||||
};
|
||||
|
||||
service.deleteManifest = function (id, repository, imageDigest) {
|
||||
return RegistryManifestsJquery.delete({
|
||||
id: id,
|
||||
repository: repository,
|
||||
tag: imageDigest
|
||||
});
|
||||
};
|
||||
|
||||
service.shortTag = function(id, repository, tag) {
|
||||
return new Promise ((resolve, reject) => {
|
||||
RegistryManifestsJquery.getV2({id:id, repository: repository, tag: tag})
|
||||
.then((data) => resolve(new RepositoryShortTag(tag, data.config.digest, data.digest, data)))
|
||||
.catch((err) => reject(err))
|
||||
});
|
||||
};
|
||||
|
||||
async function* addTagsWithProgress(id, repository, tagsList, progression = 0) {
|
||||
for await (const partialResult of genericAsyncGenerator($q, tagsList, service.addTag, [id, repository])) {
|
||||
if (typeof partialResult === 'number') {
|
||||
yield progression + partialResult;
|
||||
} else {
|
||||
yield partialResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.shortTagsWithProgress = async function* (id, repository, tagsList) {
|
||||
yield* genericAsyncGenerator($q, tagsList, service.shortTag, [id, repository]);
|
||||
}
|
||||
|
||||
async function* deleteManifestsWithProgress(id, repository, manifests) {
|
||||
for await (const partialResult of genericAsyncGenerator($q, manifests, service.deleteManifest, [id, repository])) {
|
||||
yield partialResult;
|
||||
}
|
||||
}
|
||||
|
||||
service.retagWithProgress = async function* (id, repository, modifiedTags, modifiedDigests, impactedTags){
|
||||
yield* deleteManifestsWithProgress(id, repository, modifiedDigests);
|
||||
|
||||
const newTags = _.map(impactedTags, (item) => {
|
||||
const tagFromTable = _.find(modifiedTags, { 'Name': item.Name });
|
||||
const name = tagFromTable && tagFromTable.Name !== tagFromTable.NewName ? tagFromTable.NewName : item.Name;
|
||||
return { tag: name, manifest: item.ManifestV2 };
|
||||
});
|
||||
|
||||
yield* addTagsWithProgress(id, repository, newTags, modifiedDigests.length);
|
||||
}
|
||||
|
||||
service.deleteTagsWithProgress = async function* (id, repository, modifiedDigests, impactedTags) {
|
||||
yield* deleteManifestsWithProgress(id, repository, modifiedDigests);
|
||||
|
||||
const newTags = _.map(impactedTags, (item) => {return {tag: item.Name, manifest: item.ManifestV2}})
|
||||
|
||||
yield* addTagsWithProgress(id, repository, newTags, modifiedDigests.length);
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
]);
|
|
@ -0,0 +1,13 @@
|
|||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<span class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ $ctrl.resolve.message }}
|
||||
</p>
|
||||
</span>
|
||||
<span>
|
||||
{{ $ctrl.resolve.progressLabel }} : {{ $ctrl.resolve.context.progression }}% - {{ $ctrl.resolve.context.elapsedTime |number:0 }}s
|
||||
</span>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
|
@ -0,0 +1,6 @@
|
|||
angular.module('portainer.extensions.registrymanagement').component('progressionModal', {
|
||||
templateUrl: './progressionModal.html',
|
||||
bindings: {
|
||||
resolve: '<'
|
||||
}
|
||||
});
|
|
@ -11,6 +11,31 @@
|
|||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<information-panel ng-if="!state.tagsRetrieval.auto" title-text="Information regarding repository size">
|
||||
<span class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Portainer needs to retrieve additional information to enable <code>tags modifications (addition, removal, rename)</code> and <code>repository removal</code> features.<br>
|
||||
As this repository contains more than <code>{{ state.tagsRetrieval.limit }}</code> tags, the additional retrieval wasn't started automatically.<br>
|
||||
Once started you can still navigate this page, leaving the page will cancel the retrieval process.<br>
|
||||
<br>
|
||||
<span style="font-weight: 700">Note:</span> on very large repositories or high latency environments the retrieval process can take a few minutes.
|
||||
</p>
|
||||
<button class="btn btn-sm btn-primary" ng-if="!state.tagsRetrieval.running && short.Tags.length === 0"
|
||||
ng-click="startStopRetrieval()">Start</button>
|
||||
<button class="btn btn-sm btn-danger" ng-if="state.tagsRetrieval.running"
|
||||
ng-click="startStopRetrieval()">Cancel</button>
|
||||
</span>
|
||||
<span ng-if="state.tagsRetrieval.running && state.tagsRetrieval.progression !== '100'">
|
||||
Retrieval progress : {{ state.tagsRetrieval.progression }}% - {{ state.tagsRetrieval.elapsedTime | number:0 }}s
|
||||
</span>
|
||||
<span ng-if="!state.tagsRetrieval.running && state.tagsRetrieval.progression === '100'">
|
||||
<i class="fa fa-check-circle green-icon"></i> Retrieval completed in {{ state.tagsRetrieval.elapsedTime | number:0}}s
|
||||
</span>
|
||||
</information-panel>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<rd-widget>
|
||||
|
@ -23,7 +48,7 @@
|
|||
<td>Repository</td>
|
||||
<td>
|
||||
{{ repository.Name }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeRepository()">
|
||||
<button class="btn btn-xs btn-danger" ng-if="!state.tagsRetrieval.running && state.tagsRetrieval.progression !== 0" ng-click="removeRepository()">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this repository
|
||||
</button>
|
||||
</td>
|
||||
|
@ -32,9 +57,9 @@
|
|||
<td>Tags count</td>
|
||||
<td>{{ repository.Tags.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr ng-if="short.Images.length">
|
||||
<td>Images count</td>
|
||||
<td>{{ repository.Images.length }}</td>
|
||||
<td>{{ short.Images.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -42,14 +67,16 @@
|
|||
</rd-widget>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4">
|
||||
<div class="col-sm-4" ng-if="short.Images.length > 0">
|
||||
<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>
|
||||
<label for="tag" class="col-sm-3 col-lg-2 control-label text-left">Tag
|
||||
<portainer-tooltip position="bottom" message="Tag can only contain alphanumeric (a-zA-Z0-9) and special _ . - characters. Tag must not start with . - characters."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="tag" ng-model="formValues.Tag">
|
||||
</div>
|
||||
|
@ -58,10 +85,10 @@
|
|||
<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>
|
||||
<span>{{ $select.selected | trimshasum }}</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="image in (repository.Images | filter: $select.search)">
|
||||
<span>{{ image }}</span>
|
||||
<ui-select-choices repeat="image in (short.Images | filter: $select.search)">
|
||||
<span>{{ image | trimshasum }}</span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
|
@ -83,6 +110,10 @@
|
|||
<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>
|
||||
order-by="Name" remove-action="removeTags" retag-action="retagAction"
|
||||
advanced-features-available="short.Images.length > 0"
|
||||
pagination-action="paginationAction"
|
||||
loading="state.loading">
|
||||
</registries-repository-tags-datatable>
|
||||
</div>
|
||||
</div>
|
|
@ -1,105 +1,335 @@
|
|||
import _ from 'lodash-es';
|
||||
import { RepositoryTagViewModel, RepositoryShortTag } from '../../../models/repositoryTag';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.controller('RegistryRepositoryController', ['$q', '$scope', '$transition$', '$state', 'RegistryV2Service', 'RegistryService', 'ModalService', 'Notifications',
|
||||
function ($q, $scope, $transition$, $state, RegistryV2Service, RegistryService, ModalService, Notifications) {
|
||||
.controller('RegistryRepositoryController', ['$q', '$async', '$scope', '$uibModal', '$interval', '$transition$', '$state', 'RegistryV2Service', 'RegistryService', 'ModalService', 'Notifications', 'ImageHelper',
|
||||
function ($q, $async, $scope, $uibModal, $interval, $transition$, $state, RegistryV2Service, RegistryService, ModalService, Notifications, ImageHelper) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
actionInProgress: false,
|
||||
loading: false,
|
||||
tagsRetrieval: {
|
||||
auto: true,
|
||||
running: false,
|
||||
limit: 100,
|
||||
progression: 0,
|
||||
elapsedTime: 0,
|
||||
asyncGenerator: null,
|
||||
clock: null
|
||||
},
|
||||
tagsRetag: {
|
||||
running: false,
|
||||
progression: 0,
|
||||
elapsedTime: 0,
|
||||
asyncGenerator: null,
|
||||
clock: null
|
||||
},
|
||||
tagsDelete: {
|
||||
running: false,
|
||||
progression: 0,
|
||||
elapsedTime: 0,
|
||||
asyncGenerator: null,
|
||||
clock: null
|
||||
},
|
||||
};
|
||||
$scope.formValues = {
|
||||
Tag: ''
|
||||
Tag: '' // new tag name on add feature
|
||||
};
|
||||
$scope.tags = []; // RepositoryTagViewModel (for datatable)
|
||||
$scope.short = {
|
||||
Tags: [], // RepositoryShortTag
|
||||
Images: [] // strings extracted from short.Tags
|
||||
};
|
||||
$scope.tags = [];
|
||||
$scope.repository = {
|
||||
Name: [],
|
||||
Tags: [],
|
||||
Images: []
|
||||
Name: '',
|
||||
Tags: [], // string list
|
||||
};
|
||||
|
||||
$scope.$watch('tags.length', function () {
|
||||
var images = $scope.tags.map(function (item) {
|
||||
return item.ImageId;
|
||||
function toSeconds(time) {
|
||||
return time / 1000;
|
||||
}
|
||||
function toPercent(progress, total) {
|
||||
return (progress / total * 100).toFixed();
|
||||
}
|
||||
|
||||
function openModal(resolve) {
|
||||
return $uibModal.open({
|
||||
component: 'progressionModal',
|
||||
backdrop: 'static',
|
||||
keyboard: false,
|
||||
resolve: resolve
|
||||
});
|
||||
$scope.repository.Images = _.uniq(images);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.paginationAction = function (tags) {
|
||||
$scope.state.loading = true;
|
||||
RegistryV2Service.getTagsDetails($scope.registryId, $scope.repository.Name, tags)
|
||||
.then(function success(data) {
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var idx = _.findIndex($scope.tags, {'Name': data[i].Name});
|
||||
if (idx !== -1) {
|
||||
$scope.tags[idx] = data[i];
|
||||
}
|
||||
}
|
||||
$scope.state.loading = false;
|
||||
}).catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve tags details');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* RETRIEVAL SECTION
|
||||
*/
|
||||
function updateRetrievalClock(startTime) {
|
||||
$scope.state.tagsRetrieval.elapsedTime = toSeconds(Date.now() - startTime);
|
||||
}
|
||||
|
||||
function createRetrieveAsyncGenerator() {
|
||||
$scope.state.tagsRetrieval.asyncGenerator =
|
||||
RegistryV2Service.shortTagsWithProgress($scope.registryId, $scope.repository.Name, $scope.repository.Tags);
|
||||
}
|
||||
|
||||
function resetTagsRetrievalState() {
|
||||
$scope.state.tagsRetrieval.running = false;
|
||||
$scope.state.tagsRetrieval.progression = 0;
|
||||
$scope.state.tagsRetrieval.elapsedTime = 0;
|
||||
$scope.state.tagsRetrieval.clock = null;
|
||||
}
|
||||
|
||||
function computeImages() {
|
||||
const images = _.map($scope.short.Tags, 'ImageId');
|
||||
$scope.short.Images = _.without(_.uniq(images), '');
|
||||
}
|
||||
|
||||
$scope.startStopRetrieval = function () {
|
||||
if ($scope.state.tagsRetrieval.running) {
|
||||
$scope.state.tagsRetrieval.asyncGenerator.return();
|
||||
$interval.cancel($scope.state.tagsRetrieval.clock);
|
||||
} else {
|
||||
retrieveTags().then(() => {
|
||||
createRetrieveAsyncGenerator();
|
||||
if ($scope.short.Tags.length === 0) {
|
||||
resetTagsRetrievalState();
|
||||
} else {
|
||||
computeImages();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function retrieveTags() {
|
||||
return $async(retrieveTagsAsync);
|
||||
}
|
||||
|
||||
async function retrieveTagsAsync() {
|
||||
$scope.state.tagsRetrieval.running = true;
|
||||
const startTime = Date.now();
|
||||
$scope.state.tagsRetrieval.clock = $interval(updateRetrievalClock, 1000, 0, true, startTime);
|
||||
for await (const partialResult of $scope.state.tagsRetrieval.asyncGenerator) {
|
||||
if (typeof partialResult === 'number') {
|
||||
$scope.state.tagsRetrieval.progression = toPercent(partialResult, $scope.repository.Tags.length);
|
||||
} else {
|
||||
$scope.short.Tags = _.sortBy(partialResult, 'Name');
|
||||
}
|
||||
}
|
||||
$scope.state.tagsRetrieval.running = false;
|
||||
$interval.cancel($scope.state.tagsRetrieval.clock);
|
||||
}
|
||||
/**
|
||||
* !END RETRIEVAL SECTION
|
||||
*/
|
||||
|
||||
/**
|
||||
* ADD TAG SECTION
|
||||
*/
|
||||
|
||||
async function addTagAsync() {
|
||||
try {
|
||||
$scope.state.actionInProgress = true;
|
||||
if (!ImageHelper.isValidTag($scope.formValues.Tag)) {
|
||||
throw {msg: 'Invalid tag pattern, see info for more details on format.'}
|
||||
}
|
||||
const tag = $scope.short.Tags.find((item) => item.ImageId === $scope.formValues.SelectedImage);
|
||||
const manifest = tag.ManifestV2;
|
||||
await RegistryV2Service.addTag($scope.registryId, $scope.repository.Name, {tag: $scope.formValues.Tag, manifest: manifest})
|
||||
|
||||
Notifications.success('Success', 'Tag successfully added');
|
||||
$scope.short.Tags.push(new RepositoryShortTag($scope.formValues.Tag, tag.ImageId, tag.ImageDigest, tag.ManifestV2));
|
||||
|
||||
await loadRepositoryDetails();
|
||||
$scope.formValues.Tag = '';
|
||||
delete $scope.formValues.SelectedImage;
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to add tag');
|
||||
} finally {
|
||||
$scope.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
$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');
|
||||
});
|
||||
return $async(addTagAsync);
|
||||
};
|
||||
/**
|
||||
* !END ADD TAG SECTION
|
||||
*/
|
||||
|
||||
$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;
|
||||
/**
|
||||
* RETAG SECTION
|
||||
*/
|
||||
function updateRetagClock(startTime) {
|
||||
$scope.state.tagsRetag.elapsedTime = toSeconds(Date.now() - startTime);
|
||||
}
|
||||
|
||||
function createRetagAsyncGenerator(modifiedTags, modifiedDigests, impactedTags) {
|
||||
$scope.state.tagsRetag.asyncGenerator =
|
||||
RegistryV2Service.retagWithProgress($scope.registryId, $scope.repository.Name, modifiedTags, modifiedDigests, impactedTags);
|
||||
}
|
||||
|
||||
async function retagActionAsync() {
|
||||
let modal = null;
|
||||
try {
|
||||
$scope.state.tagsRetag.running = true;
|
||||
|
||||
const modifiedTags = _.filter($scope.tags, (item) => item.Modified === true);
|
||||
for (const tag of modifiedTags) {
|
||||
if (!ImageHelper.isValidTag(tag.NewName)) {
|
||||
throw {msg: 'Invalid tag pattern, see info for more details on format.'}
|
||||
}
|
||||
}
|
||||
modal = await openModal({
|
||||
message: () => 'Retag is in progress! Closing your browser or refreshing the page while this operation is in progress will result in loss of tags.',
|
||||
progressLabel: () => 'Retag progress',
|
||||
context: () => $scope.state.tagsRetag
|
||||
});
|
||||
};
|
||||
const modifiedDigests = _.uniq(_.map(modifiedTags, 'ImageDigest'));
|
||||
const impactedTags = _.filter($scope.short.Tags, (item) => _.includes(modifiedDigests, item.ImageDigest));
|
||||
|
||||
$scope.removeTags = function (selectedItems) {
|
||||
const totalOps = modifiedDigests.length + impactedTags.length;
|
||||
|
||||
createRetagAsyncGenerator(modifiedTags, modifiedDigests, impactedTags);
|
||||
|
||||
const startTime = Date.now();
|
||||
$scope.state.tagsRetag.clock = $interval(updateRetagClock, 1000, 0, true, startTime);
|
||||
for await (const partialResult of $scope.state.tagsRetag.asyncGenerator) {
|
||||
if (typeof partialResult === 'number') {
|
||||
$scope.state.tagsRetag.progression = toPercent(partialResult, totalOps);
|
||||
}
|
||||
}
|
||||
|
||||
_.map(modifiedTags, (item) => {
|
||||
const idx = _.findIndex($scope.short.Tags, (i) => i.Name === item.Name);
|
||||
$scope.short.Tags[idx].Name = item.NewName;
|
||||
});
|
||||
|
||||
Notifications.success('Success', 'Tags successfully renamed');
|
||||
|
||||
await loadRepositoryDetails();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to rename tags');
|
||||
} finally {
|
||||
$interval.cancel($scope.state.tagsRetag.clock);
|
||||
$scope.state.tagsRetag.running = false;
|
||||
if (modal) {
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.retagAction = function() {
|
||||
return $async(retagActionAsync);
|
||||
}
|
||||
/**
|
||||
* !END RETAG SECTION
|
||||
*/
|
||||
|
||||
/**
|
||||
* REMOVE TAGS SECTION
|
||||
*/
|
||||
|
||||
function updateDeleteClock(startTime) {
|
||||
$scope.state.tagsDelete.elapsedTime = toSeconds(Date.now() - startTime);
|
||||
}
|
||||
|
||||
function createDeleteAsyncGenerator(modifiedDigests, impactedTags) {
|
||||
$scope.state.tagsDelete.asyncGenerator =
|
||||
RegistryV2Service.deleteTagsWithProgress($scope.registryId, $scope.repository.Name, modifiedDigests, impactedTags);
|
||||
}
|
||||
|
||||
async function removeTagsAsync(selectedTags) {
|
||||
let modal = null;
|
||||
try {
|
||||
$scope.state.tagsDelete.running = true;
|
||||
modal = await openModal({
|
||||
message: () => 'Tag delete is in progress! Closing your browser or refreshing the page while this operation is in progress will result in loss of tags.',
|
||||
progressLabel: () => 'Deletion progress',
|
||||
context: () => $scope.state.tagsDelete
|
||||
});
|
||||
|
||||
const deletedTagNames = _.map(selectedTags, 'Name');
|
||||
const deletedShortTags = _.filter($scope.short.Tags, (item) => _.includes(deletedTagNames, item.Name));
|
||||
const modifiedDigests = _.uniq(_.map(deletedShortTags, 'ImageDigest'));
|
||||
const impactedTags = _.filter($scope.short.Tags, (item) => _.includes(modifiedDigests, item.ImageDigest));
|
||||
const tagsToKeep = _.without(impactedTags, ...deletedShortTags);
|
||||
|
||||
const totalOps = modifiedDigests.length + tagsToKeep.length;
|
||||
|
||||
createDeleteAsyncGenerator(modifiedDigests, tagsToKeep);
|
||||
|
||||
const startTime = Date.now();
|
||||
$scope.state.tagsDelete.clock = $interval(updateDeleteClock, 1000, 0, true, startTime);
|
||||
for await (const partialResult of $scope.state.tagsDelete.asyncGenerator) {
|
||||
if (typeof partialResult === 'number') {
|
||||
$scope.state.tagsDelete.progression = toPercent(partialResult, totalOps);
|
||||
}
|
||||
}
|
||||
|
||||
_.pull($scope.short.Tags, ...deletedShortTags);
|
||||
$scope.short.Images = _.map(_.uniqBy($scope.short.Tags, 'ImageId'), 'ImageId');
|
||||
|
||||
Notifications.success('Success', 'Tags successfully deleted');
|
||||
|
||||
if ($scope.short.Tags.length === 0) {
|
||||
$state.go('portainer.registries.registry.repositories', {id: $scope.registryId}, {reload: true});
|
||||
}
|
||||
await loadRepositoryDetails();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to delete tags');
|
||||
} finally {
|
||||
$interval.cancel($scope.state.tagsDelete.clock);
|
||||
$scope.state.tagsDelete.running = false;
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.removeTags = function(selectedItems) {
|
||||
ModalService.confirmDeletion(
|
||||
'Are you sure you want to remove the selected tags ?',
|
||||
function onConfirm(confirmed) {
|
||||
(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(data) {
|
||||
Notifications.success('Success', 'Tags successfully deleted');
|
||||
if (data.length === 0) {
|
||||
$state.go('portainer.registries.registry.repositories', {
|
||||
id: $scope.registryId
|
||||
}, {
|
||||
reload: true
|
||||
});
|
||||
} else {
|
||||
$state.reload();
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to delete tags');
|
||||
});
|
||||
return $async(removeTagsAsync, selectedItems);
|
||||
});
|
||||
};
|
||||
}
|
||||
/**
|
||||
* !END REMOVE TAGS SECTION
|
||||
*/
|
||||
|
||||
/**
|
||||
* REMOVE REPOSITORY SECTION
|
||||
*/
|
||||
async function removeRepositoryAsync() {
|
||||
try {
|
||||
const digests = _.uniqBy($scope.short.Tags, 'ImageDigest');
|
||||
const promises = [];
|
||||
_.map(digests, (item) => promises.push(RegistryV2Service.deleteManifest($scope.registryId, $scope.repository.Name, item.ImageDigest)));
|
||||
await Promise.all(promises);
|
||||
Notifications.success('Success', 'Repository sucessfully removed');
|
||||
$state.go('portainer.registries.registry.repositories', {id: $scope.registryId}, {reload: true});
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to delete repository');
|
||||
}
|
||||
}
|
||||
|
||||
$scope.removeRepository = function () {
|
||||
ModalService.confirmDeletion(
|
||||
|
@ -108,53 +338,81 @@ angular.module('portainer.app')
|
|||
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');
|
||||
});
|
||||
return $async(removeRepositoryAsync);
|
||||
}
|
||||
);
|
||||
};
|
||||
/**
|
||||
* !END REMOVE REPOSITORY SECTION
|
||||
*/
|
||||
|
||||
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 = [].concat(data.tags || []);
|
||||
$scope.tags = [];
|
||||
for (var i = 0; i < $scope.repository.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');
|
||||
});
|
||||
/**
|
||||
* INIT SECTION
|
||||
*/
|
||||
async function loadRepositoryDetails() {
|
||||
try {
|
||||
const registryId = $scope.registryId;
|
||||
const repository = $scope.repository.Name;
|
||||
const tags = await RegistryV2Service.tags(registryId, repository);
|
||||
$scope.tags = [];
|
||||
$scope.repository.Tags = [];
|
||||
$scope.repository.Tags = _.sortBy(_.concat($scope.repository.Tags, _.without(tags.tags, null)));
|
||||
_.map($scope.repository.Tags, (item) => $scope.tags.push(new RepositoryTagViewModel(item)));
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve tags details');
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
async function initView() {
|
||||
try {
|
||||
const registryId = $scope.registryId = $transition$.params().id;
|
||||
$scope.repository.Name = $transition$.params().repository;
|
||||
$scope.state.loading = true;
|
||||
|
||||
$scope.registry = await RegistryService.registry(registryId);
|
||||
await loadRepositoryDetails();
|
||||
if ($scope.repository.Tags.length > $scope.state.tagsRetrieval.limit) {
|
||||
$scope.state.tagsRetrieval.auto = false;
|
||||
}
|
||||
createRetrieveAsyncGenerator();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve repository information');
|
||||
} finally {
|
||||
$scope.state.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
if ($scope.state.tagsRetrieval.asyncGenerator) {
|
||||
$scope.state.tagsRetrieval.asyncGenerator.return();
|
||||
}
|
||||
if ($scope.state.tagsRetrieval.clock) {
|
||||
$interval.cancel($scope.state.tagsRetrieval.clock);
|
||||
}
|
||||
if ($scope.state.tagsRetag.asyncGenerator) {
|
||||
$scope.state.tagsRetag.asyncGenerator.return();
|
||||
}
|
||||
if ($scope.state.tagsRetag.clock) {
|
||||
$interval.cancel($scope.state.tagsRetag.clock);
|
||||
}
|
||||
if ($scope.state.tagsDelete.asyncGenerator) {
|
||||
$scope.state.tagsDelete.asyncGenerator.return();
|
||||
}
|
||||
if ($scope.state.tagsDelete.clock) {
|
||||
$interval.cancel($scope.state.tagsDelete.clock);
|
||||
}
|
||||
});
|
||||
|
||||
this.$onInit = function() {
|
||||
return $async(initView)
|
||||
.then(() => {
|
||||
if ($scope.state.tagsRetrieval.auto) {
|
||||
$scope.startStopRetrieval();
|
||||
}
|
||||
});
|
||||
};
|
||||
/**
|
||||
* !END INIT SECTION
|
||||
*/
|
||||
}
|
||||
]);
|
||||
]);
|
|
@ -5,7 +5,7 @@
|
|||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.registries">Registries</a> > <a ng-if="isAdmin" ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a><span ng-if="!isAdmin">{{ registry.Name}}</span> > Repositories
|
||||
<a ui-sref="portainer.registries">Registries</a> > <a ng-if="isAdmin" ui-sref="portainer.registries.registry({id: registry.Id})" ui-sref-opts="{reload:true}">{{ registry.Name }}</a><span ng-if="!isAdmin">{{ registry.Name}}</span> > Repositories
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
@ -31,7 +31,7 @@
|
|||
<registry-repositories-datatable
|
||||
title-text="Repositories" title-icon="fa-book"
|
||||
dataset="repositories" table-key="registryRepositories"
|
||||
order-by="Name">
|
||||
order-by="Name" pagination-action="paginationAction" loading="state.loading">
|
||||
</registry-repositories-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,26 +1,49 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.extensions.registrymanagement')
|
||||
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications', 'Authentication',
|
||||
function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications, Authentication) {
|
||||
|
||||
$scope.state = {
|
||||
displayInvalidConfigurationMessage: false
|
||||
displayInvalidConfigurationMessage: false,
|
||||
loading: false
|
||||
};
|
||||
|
||||
$scope.paginationAction = function (repositories) {
|
||||
$scope.state.loading = true;
|
||||
RegistryV2Service.getRepositoriesDetails($scope.state.registryId, repositories)
|
||||
.then(function success(data) {
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var idx = _.findIndex($scope.repositories, {'Name': data[i].Name});
|
||||
if (idx !== -1) {
|
||||
if (data[i].TagsCount === 0) {
|
||||
$scope.repositories.splice(idx, 1);
|
||||
} else {
|
||||
$scope.repositories[idx].TagsCount = data[i].TagsCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
$scope.state.loading = false;
|
||||
}).catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve repositories details');
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
var registryId = $transition$.params().id;
|
||||
$scope.state.registryId = $transition$.params().id;
|
||||
|
||||
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||
if (authenticationEnabled) {
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
}
|
||||
|
||||
RegistryService.registry(registryId)
|
||||
RegistryService.registry($scope.state.registryId)
|
||||
.then(function success(data) {
|
||||
$scope.registry = data;
|
||||
|
||||
RegistryV2Service.ping(registryId, false)
|
||||
RegistryV2Service.ping($scope.state.registryId, false)
|
||||
.then(function success() {
|
||||
return RegistryV2Service.repositories(registryId);
|
||||
return RegistryV2Service.catalog($scope.state.registryId);
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.repositories = data;
|
||||
|
|
|
@ -27,6 +27,11 @@ function ($interval, PaginationService, DatatableService, PAGINATION_MAX_ITEMS)
|
|||
refreshRate: '30'
|
||||
}
|
||||
}
|
||||
this.resetSelectionState = function() {
|
||||
this.state.selectAll = false;
|
||||
this.state.selectedItems = [];
|
||||
_.map(this.state.filteredDataSet, (item) => item.Checked = false);
|
||||
};
|
||||
|
||||
this.onTextFilterChange = function() {
|
||||
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
||||
|
|
|
@ -98,6 +98,20 @@ angular.module('portainer.app')
|
|||
});
|
||||
};
|
||||
|
||||
service.cancelRegistryRepositoryAction = function(callback) {
|
||||
service.confirm({
|
||||
title: 'Are you sure?',
|
||||
message: 'WARNING: interrupting this operation before it has finished will result in the loss of all tags. Are you sure you want to do this?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Stop',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: callback
|
||||
});
|
||||
};
|
||||
|
||||
service.confirmDeletion = function(message, callback) {
|
||||
message = $sanitize(message);
|
||||
service.confirm({
|
||||
|
|
|
@ -834,6 +834,26 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
|||
margin: 20px auto 10px auto;
|
||||
}
|
||||
|
||||
.modal {
|
||||
text-align: center;
|
||||
padding: 0!important;
|
||||
}
|
||||
|
||||
.modal::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
/*bootbox override*/
|
||||
.modal-open {
|
||||
padding-right: 0 !important;
|
||||
|
|
|
@ -95,8 +95,8 @@
|
|||
"clean-webpack-plugin": "^0.1.19",
|
||||
"css-loader": "^1.0.0",
|
||||
"cssnano": "^3.10.0",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-loader": "^2.1.1",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-loader": "^2.1.2",
|
||||
"file-loader": "^1.1.11",
|
||||
"grunt": "~0.4.0",
|
||||
"grunt-cli": "^1.2.0",
|
||||
|
|
404
yarn.lock
404
yarn.lock
|
@ -830,6 +830,11 @@ acorn-jsx@^3.0.0:
|
|||
dependencies:
|
||||
acorn "^3.0.4"
|
||||
|
||||
acorn-jsx@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
|
||||
integrity sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==
|
||||
|
||||
acorn@^3.0.4:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
|
||||
|
@ -845,6 +850,11 @@ acorn@^5.2.1:
|
|||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7"
|
||||
integrity sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==
|
||||
|
||||
acorn@^6.0.7:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f"
|
||||
integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==
|
||||
|
||||
active-x-obfuscator@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/active-x-obfuscator/-/active-x-obfuscator-0.0.1.tgz#089b89b37145ff1d9ec74af6530be5526cae1f1a"
|
||||
|
@ -885,6 +895,16 @@ ajv@^6.1.0:
|
|||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^6.9.1:
|
||||
version "6.10.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
|
||||
integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
|
||||
dependencies:
|
||||
fast-deep-equal "^2.0.1"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
align-text@^0.1.1, align-text@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
|
||||
|
@ -1031,6 +1051,11 @@ ansi-escapes@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
|
||||
integrity sha1-06ioOzGapneTZisT52HHkRQiMG4=
|
||||
|
||||
ansi-escapes@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
|
||||
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
|
||||
|
||||
ansi-html@0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
|
||||
|
@ -1046,6 +1071,11 @@ ansi-regex@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
|
||||
integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
|
||||
|
||||
ansi-regex@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
|
||||
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
|
||||
|
||||
ansi-styles@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
||||
|
@ -1058,7 +1088,7 @@ ansi-styles@^3.1.0:
|
|||
dependencies:
|
||||
color-convert "^1.9.0"
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
ansi-styles@^3.2.0, ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
|
||||
|
@ -1254,6 +1284,11 @@ ast-types@0.9.6:
|
|||
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
|
||||
integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=
|
||||
|
||||
astral-regex@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
|
||||
integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
|
||||
|
||||
async-done@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/async-done/-/async-done-0.4.0.tgz#ab8053f5f62290f8bfc58f37cd9b73070b3307b9"
|
||||
|
@ -1954,6 +1989,11 @@ callsites@^0.2.0:
|
|||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
|
||||
integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=
|
||||
|
||||
callsites@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||
|
||||
camel-case@3.0.x:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
|
||||
|
@ -2063,6 +2103,15 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1:
|
|||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chalk@^2.1.0, chalk@^2.4.2:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
|
||||
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
|
||||
dependencies:
|
||||
ansi-styles "^3.2.1"
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chalk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
|
||||
|
@ -2072,6 +2121,11 @@ chalk@^2.3.0:
|
|||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^4.0.0"
|
||||
|
||||
chardet@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
||||
|
||||
chart.js@~2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.6.0.tgz#308f9a4b0bfed5a154c14f5deb1d9470d22abe71"
|
||||
|
@ -2210,6 +2264,13 @@ cli-cursor@^1.0.1:
|
|||
dependencies:
|
||||
restore-cursor "^1.0.1"
|
||||
|
||||
cli-cursor@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
|
||||
integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
|
||||
dependencies:
|
||||
restore-cursor "^2.0.0"
|
||||
|
||||
cli-width@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
|
||||
|
@ -3022,6 +3083,13 @@ debug@^3.1.0, debug@^3.2.5:
|
|||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.0.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
|
||||
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@~2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
|
||||
|
@ -3368,6 +3436,13 @@ doctrine@^2.0.0:
|
|||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
doctrine@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
|
||||
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
dom-converter@~0.2:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
|
||||
|
@ -3576,6 +3651,11 @@ elliptic@^6.0.0:
|
|||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.0"
|
||||
|
||||
emoji-regex@^7.0.1:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
||||
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
|
||||
|
||||
emojis-list@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||
|
@ -3769,10 +3849,10 @@ escope@^3.6.0:
|
|||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-loader@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.1.1.tgz#2a9251523652430bfdd643efdb0afc1a2a89546a"
|
||||
integrity sha512-1GrJFfSevQdYpoDzx8mEE2TDWsb/zmFuY09l6hURg1AeFIKQOvZ+vH0UPjzmd1CZIbfTV5HUkMeBmFiDBkgIsQ==
|
||||
eslint-loader@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.1.2.tgz#453542a1230d6ffac90e4e7cb9cadba9d851be68"
|
||||
integrity sha512-rA9XiXEOilLYPOIInvVH5S/hYfyTPyxag6DZhoQOduM+3TkghAEQ3VcFO8VnX4J4qg/UIBzp72aOf/xvYmpmsg==
|
||||
dependencies:
|
||||
loader-fs-cache "^1.0.0"
|
||||
loader-utils "^1.0.2"
|
||||
|
@ -3788,7 +3868,67 @@ eslint-scope@^4.0.0:
|
|||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint@^3.0.0, eslint@^3.19.0:
|
||||
eslint-scope@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
|
||||
integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
|
||||
dependencies:
|
||||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-utils@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512"
|
||||
integrity sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==
|
||||
|
||||
eslint-visitor-keys@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
|
||||
integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
|
||||
|
||||
eslint@5.16.0:
|
||||
version "5.16.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea"
|
||||
integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0"
|
||||
ajv "^6.9.1"
|
||||
chalk "^2.1.0"
|
||||
cross-spawn "^6.0.5"
|
||||
debug "^4.0.1"
|
||||
doctrine "^3.0.0"
|
||||
eslint-scope "^4.0.3"
|
||||
eslint-utils "^1.3.1"
|
||||
eslint-visitor-keys "^1.0.0"
|
||||
espree "^5.0.1"
|
||||
esquery "^1.0.1"
|
||||
esutils "^2.0.2"
|
||||
file-entry-cache "^5.0.1"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
glob "^7.1.2"
|
||||
globals "^11.7.0"
|
||||
ignore "^4.0.6"
|
||||
import-fresh "^3.0.0"
|
||||
imurmurhash "^0.1.4"
|
||||
inquirer "^6.2.2"
|
||||
js-yaml "^3.13.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
levn "^0.3.0"
|
||||
lodash "^4.17.11"
|
||||
minimatch "^3.0.4"
|
||||
mkdirp "^0.5.1"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.8.2"
|
||||
path-is-inside "^1.0.2"
|
||||
progress "^2.0.0"
|
||||
regexpp "^2.0.1"
|
||||
semver "^5.5.1"
|
||||
strip-ansi "^4.0.0"
|
||||
strip-json-comments "^2.0.1"
|
||||
table "^5.2.3"
|
||||
text-table "^0.2.0"
|
||||
|
||||
eslint@^3.0.0:
|
||||
version "3.19.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc"
|
||||
integrity sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=
|
||||
|
@ -3837,6 +3977,15 @@ espree@^3.4.0:
|
|||
acorn "^5.2.1"
|
||||
acorn-jsx "^3.0.0"
|
||||
|
||||
espree@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a"
|
||||
integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==
|
||||
dependencies:
|
||||
acorn "^6.0.7"
|
||||
acorn-jsx "^5.0.0"
|
||||
eslint-visitor-keys "^1.0.0"
|
||||
|
||||
esprima@1.0.x, "esprima@~ 1.0.2", esprima@~1.0.2:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.0.4.tgz#9f557e08fc3b4d26ece9dd34f8fbf476b62585ad"
|
||||
|
@ -3864,6 +4013,13 @@ esquery@^1.0.0:
|
|||
dependencies:
|
||||
estraverse "^4.0.0"
|
||||
|
||||
esquery@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
|
||||
integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==
|
||||
dependencies:
|
||||
estraverse "^4.0.0"
|
||||
|
||||
esrecurse@^4.1.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
|
||||
|
@ -4157,6 +4313,15 @@ extend@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
||||
integrity sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=
|
||||
|
||||
external-editor@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27"
|
||||
integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==
|
||||
dependencies:
|
||||
chardet "^0.7.0"
|
||||
iconv-lite "^0.4.24"
|
||||
tmp "^0.0.33"
|
||||
|
||||
extglob@^0.3.1:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
|
||||
|
@ -4245,6 +4410,13 @@ figures@^1.0.1, figures@^1.3.5:
|
|||
escape-string-regexp "^1.0.5"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
figures@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
|
||||
integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=
|
||||
dependencies:
|
||||
escape-string-regexp "^1.0.5"
|
||||
|
||||
file-entry-cache@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
|
||||
|
@ -4253,6 +4425,13 @@ file-entry-cache@^2.0.0:
|
|||
flat-cache "^1.2.1"
|
||||
object-assign "^4.0.1"
|
||||
|
||||
file-entry-cache@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
|
||||
integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
|
||||
dependencies:
|
||||
flat-cache "^2.0.1"
|
||||
|
||||
file-loader@^1.1.11:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8"
|
||||
|
@ -4519,6 +4698,20 @@ flat-cache@^1.2.1:
|
|||
graceful-fs "^4.1.2"
|
||||
write "^0.2.1"
|
||||
|
||||
flat-cache@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
|
||||
integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
|
||||
dependencies:
|
||||
flatted "^2.0.0"
|
||||
rimraf "2.6.3"
|
||||
write "1.0.3"
|
||||
|
||||
flatted@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916"
|
||||
integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==
|
||||
|
||||
flatten@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
|
||||
|
@ -4652,6 +4845,11 @@ function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
|
||||
functional-red-black-tree@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||
|
||||
gauge@~2.7.3:
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||
|
@ -4873,6 +5071,18 @@ glob@^7.0.5, glob@^7.1.2:
|
|||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.1.4"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
|
||||
integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@~3.1.21:
|
||||
version "3.1.21"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd"
|
||||
|
@ -4910,6 +5120,11 @@ globals@^11.1.0:
|
|||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.8.0.tgz#c1ef45ee9bed6badf0663c5cb90e8d1adec1321d"
|
||||
integrity sha512-io6LkyPVuzCHBSQV9fmOwxZkUk6nIaGmxheLDgmuFv89j0fm2aqDbIXKAGfzCMHqz3HLF2Zf8WSG6VqMh2qFmA==
|
||||
|
||||
globals@^11.7.0:
|
||||
version "11.12.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
|
||||
|
||||
globals@^9.14.0:
|
||||
version "9.18.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
|
||||
|
@ -5772,7 +5987,7 @@ iconv-lite@0.4.23:
|
|||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
iconv-lite@^0.4.4:
|
||||
iconv-lite@^0.4.24, iconv-lite@^0.4.4:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
|
@ -5818,6 +6033,11 @@ ignore@^3.2.0:
|
|||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
|
||||
integrity sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==
|
||||
|
||||
ignore@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||
|
||||
image-webpack-loader@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/image-webpack-loader/-/image-webpack-loader-4.5.0.tgz#ab0da4302a58f2bf7a2eb62f166c82c6495efe8d"
|
||||
|
@ -5906,6 +6126,14 @@ import-cwd@^2.0.0:
|
|||
dependencies:
|
||||
import-from "^2.1.0"
|
||||
|
||||
import-fresh@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.0.0.tgz#a3d897f420cab0e671236897f75bc14b4885c390"
|
||||
integrity sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==
|
||||
dependencies:
|
||||
parent-module "^1.0.0"
|
||||
resolve-from "^4.0.0"
|
||||
|
||||
import-from@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1"
|
||||
|
@ -5995,6 +6223,25 @@ inquirer@^0.12.0:
|
|||
strip-ansi "^3.0.0"
|
||||
through "^2.3.6"
|
||||
|
||||
inquirer@^6.2.2:
|
||||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.4.1.tgz#7bd9e5ab0567cd23b41b0180b68e0cfa82fc3c0b"
|
||||
integrity sha512-/Jw+qPZx4EDYsaT6uz7F4GJRNFMRdKNeUZw3ZnKV8lyuUgz/YWRCSUAJMZSVhSq4Ec0R2oYnyi6b3d4JXcL5Nw==
|
||||
dependencies:
|
||||
ansi-escapes "^3.2.0"
|
||||
chalk "^2.4.2"
|
||||
cli-cursor "^2.1.0"
|
||||
cli-width "^2.0.0"
|
||||
external-editor "^3.0.3"
|
||||
figures "^2.0.0"
|
||||
lodash "^4.17.11"
|
||||
mute-stream "0.0.7"
|
||||
run-async "^2.2.0"
|
||||
rxjs "^6.4.0"
|
||||
string-width "^2.1.0"
|
||||
strip-ansi "^5.1.0"
|
||||
through "^2.3.6"
|
||||
|
||||
internal-ip@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-3.0.1.tgz#df5c99876e1d2eb2ea2d74f520e3f669a00ece27"
|
||||
|
@ -6351,6 +6598,11 @@ is-primitive@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
|
||||
integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU=
|
||||
|
||||
is-promise@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
|
||||
integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
|
||||
|
||||
is-property@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
|
||||
|
@ -6547,7 +6799,7 @@ js-tokens@^3.0.2:
|
|||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
|
||||
|
||||
js-yaml@^3.12.0, js-yaml@^3.3.0, js-yaml@^3.5.1, js-yaml@^3.9.0, js-yaml@~3.13.1:
|
||||
js-yaml@^3.12.0, js-yaml@^3.13.0, js-yaml@^3.3.0, js-yaml@^3.5.1, js-yaml@^3.9.0, js-yaml@~3.13.1:
|
||||
version "3.13.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
|
||||
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
|
||||
|
@ -6610,6 +6862,11 @@ json-schema-traverse@^0.4.1:
|
|||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
|
||||
|
||||
json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
|
||||
|
@ -7064,7 +7321,7 @@ lodash@^4.17.10:
|
|||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
|
||||
integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==
|
||||
|
||||
lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.11:
|
||||
lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.11:
|
||||
version "4.17.11"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||
|
@ -7650,6 +7907,11 @@ mute-stream@0.0.5:
|
|||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
|
||||
integrity sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=
|
||||
|
||||
mute-stream@0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
|
||||
|
||||
nan@^2.9.2:
|
||||
version "2.11.1"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
|
||||
|
@ -8087,6 +8349,13 @@ onetime@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
|
||||
integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=
|
||||
|
||||
onetime@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
|
||||
integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
|
||||
dependencies:
|
||||
mimic-fn "^1.0.0"
|
||||
|
||||
opn@^5.1.0:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035"
|
||||
|
@ -8195,7 +8464,7 @@ os-locale@^3.0.0:
|
|||
lcid "^2.0.0"
|
||||
mem "^4.0.0"
|
||||
|
||||
os-tmpdir@^1.0.0:
|
||||
os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
|
||||
|
@ -8347,6 +8616,13 @@ param-case@2.1.x:
|
|||
dependencies:
|
||||
no-case "^2.2.0"
|
||||
|
||||
parent-module@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
|
||||
dependencies:
|
||||
callsites "^3.0.0"
|
||||
|
||||
parse-asn1@^5.0.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8"
|
||||
|
@ -8439,7 +8715,7 @@ path-is-absolute@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
|
||||
|
||||
path-is-inside@^1.0.1:
|
||||
path-is-inside@^1.0.1, path-is-inside@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
|
||||
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
|
||||
|
@ -9001,6 +9277,11 @@ progress@^1.1.8:
|
|||
resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
|
||||
integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=
|
||||
|
||||
progress@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||
|
||||
promise-inflight@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||
|
@ -9403,6 +9684,11 @@ regex-not@^1.0.0, regex-not@^1.0.2:
|
|||
extend-shallow "^3.0.2"
|
||||
safe-regex "^1.1.0"
|
||||
|
||||
regexpp@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
|
||||
integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
|
||||
|
||||
regexpu-core@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
|
||||
|
@ -9579,6 +9865,11 @@ resolve-from@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
|
||||
integrity sha1-six699nWiBvItuZTM17rywoYh0g=
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||
|
||||
resolve-pkg@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-pkg/-/resolve-pkg-0.1.0.tgz#02cc993410e2936962bd97166a1b077da9725531"
|
||||
|
@ -9631,6 +9922,14 @@ restore-cursor@^1.0.1:
|
|||
exit-hook "^1.0.0"
|
||||
onetime "^1.0.0"
|
||||
|
||||
restore-cursor@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
|
||||
integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
|
||||
dependencies:
|
||||
onetime "^2.0.0"
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
ret@~0.1.10:
|
||||
version "0.1.15"
|
||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||
|
@ -9643,6 +9942,13 @@ right-align@^0.1.1:
|
|||
dependencies:
|
||||
align-text "^0.1.1"
|
||||
|
||||
rimraf@2.6.3:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@2.x.x, rimraf@^2.2.8, rimraf@~2.2.8:
|
||||
version "2.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582"
|
||||
|
@ -9682,6 +9988,13 @@ run-async@^0.1.0:
|
|||
dependencies:
|
||||
once "^1.3.0"
|
||||
|
||||
run-async@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
|
||||
integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA=
|
||||
dependencies:
|
||||
is-promise "^2.1.0"
|
||||
|
||||
run-queue@^1.0.0, run-queue@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
|
||||
|
@ -9694,6 +10007,13 @@ rx-lite@^3.1.2:
|
|||
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
|
||||
integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=
|
||||
|
||||
rxjs@^6.4.0:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7"
|
||||
integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
|
@ -9789,6 +10109,11 @@ semver@^5.4.1, semver@^5.6.0:
|
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
|
||||
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
|
||||
|
||||
semver@^5.5.1:
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
|
||||
integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
|
||||
|
||||
send@0.13.2:
|
||||
version "0.13.2"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.13.2.tgz#765e7607c8055452bba6f0b052595350986036de"
|
||||
|
@ -9975,7 +10300,7 @@ sigmund@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
|
||||
integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=
|
||||
|
||||
signal-exit@^3.0.0:
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
|
||||
|
@ -9990,6 +10315,15 @@ slice-ansi@0.0.4:
|
|||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
|
||||
integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=
|
||||
|
||||
slice-ansi@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
|
||||
integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
|
||||
dependencies:
|
||||
ansi-styles "^3.2.0"
|
||||
astral-regex "^1.0.0"
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
|
||||
snapdragon-node@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
|
||||
|
@ -10347,7 +10681,7 @@ string-width@^1.0.1:
|
|||
is-fullwidth-code-point "^1.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1:
|
||||
"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
|
||||
|
@ -10355,6 +10689,15 @@ string-width@^1.0.1:
|
|||
is-fullwidth-code-point "^2.0.0"
|
||||
strip-ansi "^4.0.0"
|
||||
|
||||
string-width@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
|
||||
integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
|
||||
dependencies:
|
||||
emoji-regex "^7.0.1"
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
strip-ansi "^5.1.0"
|
||||
|
||||
string_decoder@^1.0.0, string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
|
@ -10393,6 +10736,13 @@ strip-ansi@^4.0.0:
|
|||
dependencies:
|
||||
ansi-regex "^3.0.0"
|
||||
|
||||
strip-ansi@^5.1.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
|
||||
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
|
||||
dependencies:
|
||||
ansi-regex "^4.1.0"
|
||||
|
||||
strip-bom-stream@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz#e7144398577d51a6bed0fa1994fa05f43fd988ee"
|
||||
|
@ -10457,7 +10807,7 @@ strip-json-comments@1.0.x:
|
|||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
|
||||
integrity sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=
|
||||
|
||||
strip-json-comments@~2.0.1:
|
||||
strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||
|
@ -10555,6 +10905,16 @@ table@^3.7.8:
|
|||
slice-ansi "0.0.4"
|
||||
string-width "^2.0.0"
|
||||
|
||||
table@^5.2.3:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/table/-/table-5.4.1.tgz#0691ae2ebe8259858efb63e550b6d5f9300171e8"
|
||||
integrity sha512-E6CK1/pZe2N75rGZQotFOdmzWQ1AILtgYbMAbAjvms0S1l5IDB47zG3nCnFGB/w+7nB3vKofbLXCH7HPBo864w==
|
||||
dependencies:
|
||||
ajv "^6.9.1"
|
||||
lodash "^4.17.11"
|
||||
slice-ansi "^2.1.0"
|
||||
string-width "^3.0.0"
|
||||
|
||||
tapable@^1.0.0, tapable@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.0.tgz#0d076a172e3d9ba088fd2272b2668fb8d194b78c"
|
||||
|
@ -10630,7 +10990,7 @@ terser@^3.8.1:
|
|||
source-map "~0.6.1"
|
||||
source-map-support "~0.5.6"
|
||||
|
||||
text-table@~0.2.0:
|
||||
text-table@^0.2.0, text-table@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
||||
|
@ -10731,6 +11091,13 @@ tinycolor@0.x:
|
|||
resolved "https://registry.yarnpkg.com/tinycolor/-/tinycolor-0.0.1.tgz#320b5a52d83abb5978d81a3e887d4aefb15a6164"
|
||||
integrity sha1-MgtaUtg6u1l42Bo+iH1K77FaYWQ=
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
|
||||
dependencies:
|
||||
os-tmpdir "~1.0.2"
|
||||
|
||||
to-absolute-glob@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz#1cdfa472a9ef50c239ee66999b662ca0eb39937f"
|
||||
|
@ -11618,6 +11985,13 @@ wrappy@1:
|
|||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
|
||||
write@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
|
||||
integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
|
||||
dependencies:
|
||||
mkdirp "^0.5.1"
|
||||
|
||||
write@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
|
||||
|
|
Loading…
Reference in New Issue