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 fix
pull/3292/head
xAt0mZ 2019-10-14 15:45:09 +02:00 committed by GitHub
parent 8a8cef9b20
commit 2445a5aed5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1372 additions and 421 deletions

View File

@ -5,7 +5,7 @@
"@babel/preset-env",
{
"modules": false,
"useBuiltIns": "usage"
"useBuiltIns": "entry"
}
]
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: '<'
}
});

View File

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

View File

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

View File

@ -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: '<'
}
});

View File

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

View File

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

View File

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

View File

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

View File

@ -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'
}
}
});
}]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
&nbsp; {{ $ctrl.resolve.progressLabel }} : {{ $ctrl.resolve.context.progression }}% - {{ $ctrl.resolve.context.elapsedTime |number:0 }}s
</span>
</rd-widget-body>
</rd-widget>

View File

@ -0,0 +1,6 @@
angular.module('portainer.extensions.registrymanagement').component('progressionModal', {
templateUrl: './progressionModal.html',
bindings: {
resolve: '<'
}
});

View File

@ -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'">
&nbsp; 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>

View File

@ -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
*/
}
]);
]);

View File

@ -5,7 +5,7 @@
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="portainer.registries">Registries</a> &gt; <a ng-if="isAdmin" ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a><span ng-if="!isAdmin">{{ registry.Name}}</span> &gt; Repositories
<a ui-sref="portainer.registries">Registries</a> &gt; <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> &gt; 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>

View File

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

View File

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

View File

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

View File

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

View File

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

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