1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +02:00

chore(project): add prettier for code format (#3645)

* chore(project): install prettier and lint-staged

* chore(project): apply prettier to html too

* chore(project): git ignore eslintcache

* chore(project): add a comment about format script

* chore(prettier): update printWidth

* chore(prettier): remove useTabs option

* chore(prettier): add HTML validation

* refactor(prettier): fix closing tags

* feat(prettier): define angular parser for html templates

* style(prettier): run prettier on codebase

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
This commit is contained in:
Chaim Lev-Ari 2020-04-11 00:54:53 +03:00 committed by GitHub
parent 6663073be1
commit cf5056d9c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
714 changed files with 31228 additions and 28305 deletions

View file

@ -1,5 +1 @@
angular.module('portainer.extensions', [
'portainer.extensions.registrymanagement',
'portainer.extensions.oauth',
'portainer.extensions.rbac'
]);
angular.module('portainer.extensions', ['portainer.extensions.registrymanagement', 'portainer.extensions.oauth', 'portainer.extensions.rbac']);

View file

@ -1,2 +1 @@
angular.module('portainer.extensions.oauth', ['ngResource'])
.constant('API_ENDPOINT_OAUTH', 'api/auth/oauth');
angular.module('portainer.extensions.oauth', ['ngResource']).constant('API_ENDPOINT_OAUTH', 'api/auth/oauth');

View file

@ -1,63 +1,60 @@
angular.module('portainer.extensions.oauth')
.controller('OAuthProviderSelectorController', function OAuthProviderSelectorController() {
var ctrl = this;
angular.module('portainer.extensions.oauth').controller('OAuthProviderSelectorController', function OAuthProviderSelectorController() {
var ctrl = this;
this.providers = [
{
authUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/authorize',
accessTokenUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/token',
resourceUrl: 'https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08',
userIdentifier: 'userPrincipalName',
scopes: 'id,email,name',
name: 'microsoft'
},
{
authUrl: 'https://accounts.google.com/o/oauth2/auth',
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
resourceUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
userIdentifier: 'email',
scopes: 'profile email',
name: 'google'
},
{
authUrl: 'https://github.com/login/oauth/authorize',
accessTokenUrl: 'https://github.com/login/oauth/access_token',
resourceUrl: 'https://api.github.com/user',
userIdentifier: 'login',
scopes: 'id email name',
name: 'github'
},
{
authUrl: '',
accessTokenUrl: '',
resourceUrl: '',
userIdentifier: '',
scopes: '',
name: 'custom'
}
];
this.providers = [
{
authUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/authorize',
accessTokenUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/token',
resourceUrl: 'https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08',
userIdentifier: 'userPrincipalName',
scopes: 'id,email,name',
name: 'microsoft',
},
{
authUrl: 'https://accounts.google.com/o/oauth2/auth',
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
resourceUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
userIdentifier: 'email',
scopes: 'profile email',
name: 'google',
},
{
authUrl: 'https://github.com/login/oauth/authorize',
accessTokenUrl: 'https://github.com/login/oauth/access_token',
resourceUrl: 'https://api.github.com/user',
userIdentifier: 'login',
scopes: 'id email name',
name: 'github',
},
{
authUrl: '',
accessTokenUrl: '',
resourceUrl: '',
userIdentifier: '',
scopes: '',
name: 'custom',
},
];
this.$onInit = onInit;
this.$onInit = onInit;
function onInit() {
if (ctrl.provider.authUrl) {
ctrl.provider = getProviderByURL(ctrl.provider.authUrl);
} else {
ctrl.provider = ctrl.providers[0];
}
ctrl.onSelect(ctrl.provider, false);
function onInit() {
if (ctrl.provider.authUrl) {
ctrl.provider = getProviderByURL(ctrl.provider.authUrl);
} else {
ctrl.provider = ctrl.providers[0];
}
ctrl.onSelect(ctrl.provider, false);
}
function getProviderByURL(providerAuthURL) {
if (providerAuthURL.indexOf('login.microsoftonline.com') !== -1) {
return ctrl.providers[0];
}
else if (providerAuthURL.indexOf('accounts.google.com') !== -1) {
return ctrl.providers[1];
}
else if (providerAuthURL.indexOf('github.com') !== -1) {
return ctrl.providers[2];
}
return ctrl.providers[3];
function getProviderByURL(providerAuthURL) {
if (providerAuthURL.indexOf('login.microsoftonline.com') !== -1) {
return ctrl.providers[0];
} else if (providerAuthURL.indexOf('accounts.google.com') !== -1) {
return ctrl.providers[1];
} else if (providerAuthURL.indexOf('github.com') !== -1) {
return ctrl.providers[2];
}
});
return ctrl.providers[3];
}
});

View file

@ -3,10 +3,10 @@
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0">
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
<input type="radio" id="oauth_provider_microsoft" ng-model="$ctrl.provider" ng-value="$ctrl.providers[0]">
<input type="radio" id="oauth_provider_microsoft" ng-model="$ctrl.provider" ng-value="$ctrl.providers[0]" />
<label for="oauth_provider_microsoft">
<div class="boxselector_header">
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
@ -16,7 +16,7 @@
</label>
</div>
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
<input type="radio" id="oauth_provider_google" ng-model="$ctrl.provider" ng-value="$ctrl.providers[1]">
<input type="radio" id="oauth_provider_google" ng-model="$ctrl.provider" ng-value="$ctrl.providers[1]" />
<label for="oauth_provider_google">
<div class="boxselector_header">
<i class="fab fa-google" aria-hidden="true" style="margin-right: 2px;"></i>
@ -26,7 +26,7 @@
</label>
</div>
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
<input type="radio" id="oauth_provider_github" ng-model="$ctrl.provider" ng-value="$ctrl.providers[2]">
<input type="radio" id="oauth_provider_github" ng-model="$ctrl.provider" ng-value="$ctrl.providers[2]" />
<label for="oauth_provider_github">
<div class="boxselector_header">
<i class="fab fa-github" aria-hidden="true" style="margin-right: 2px;"></i>
@ -36,7 +36,7 @@
</label>
</div>
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
<input type="radio" id="oauth_provider_custom" ng-model="$ctrl.provider" ng-value="$ctrl.providers[3]">
<input type="radio" id="oauth_provider_custom" ng-model="$ctrl.provider" ng-value="$ctrl.providers[3]" />
<label for="oauth_provider_custom">
<div class="boxselector_header">
<i class="fa fa-user-check" aria-hidden="true" style="margin-right: 2px;"></i>

View file

@ -2,7 +2,7 @@ angular.module('portainer.extensions.oauth').component('oauthProvidersSelector',
templateUrl: './oauth-providers-selector.html',
bindings: {
onSelect: '<',
provider: '='
provider: '=',
},
controller: 'OAuthProviderSelectorController'
controller: 'OAuthProviderSelectorController',
});

View file

@ -1,76 +1,75 @@
import _ from 'lodash-es';
angular.module('portainer.extensions.oauth')
.controller('OAuthSettingsController', function OAuthSettingsController() {
var ctrl = this;
angular.module('portainer.extensions.oauth').controller('OAuthSettingsController', function OAuthSettingsController() {
var ctrl = this;
this.state = {
provider: {},
overrideConfiguration: false,
microsoftTenantID: ''
};
this.state = {
provider: {},
overrideConfiguration: false,
microsoftTenantID: '',
};
this.$onInit = onInit;
this.onSelectProvider = onSelectProvider;
this.onMicrosoftTenantIDChange = onMicrosoftTenantIDChange;
this.useDefaultProviderConfiguration = useDefaultProviderConfiguration;
this.$onInit = onInit;
this.onSelectProvider = onSelectProvider;
this.onMicrosoftTenantIDChange = onMicrosoftTenantIDChange;
this.useDefaultProviderConfiguration = useDefaultProviderConfiguration;
function onMicrosoftTenantIDChange() {
var tenantID = ctrl.state.microsoftTenantID;
function onMicrosoftTenantIDChange() {
var tenantID = ctrl.state.microsoftTenantID;
ctrl.settings.AuthorizationURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/authorize', 'TENANT_ID', tenantID);
ctrl.settings.AccessTokenURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/token', 'TENANT_ID', tenantID);
ctrl.settings.ResourceURI = _.replace('https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08', 'TENANT_ID', tenantID);
ctrl.settings.AuthorizationURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/authorize', 'TENANT_ID', tenantID);
ctrl.settings.AccessTokenURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/token', 'TENANT_ID', tenantID);
ctrl.settings.ResourceURI = _.replace('https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08', 'TENANT_ID', tenantID);
}
function useDefaultProviderConfiguration() {
ctrl.settings.AuthorizationURI = ctrl.state.provider.authUrl;
ctrl.settings.AccessTokenURI = ctrl.state.provider.accessTokenUrl;
ctrl.settings.ResourceURI = ctrl.state.provider.resourceUrl;
ctrl.settings.UserIdentifier = ctrl.state.provider.userIdentifier;
ctrl.settings.Scopes = ctrl.state.provider.scopes;
if (ctrl.state.provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') {
onMicrosoftTenantIDChange();
}
}
function useExistingConfiguration() {
var provider = ctrl.state.provider;
ctrl.settings.AuthorizationURI = ctrl.settings.AuthorizationURI === '' ? provider.authUrl : ctrl.settings.AuthorizationURI;
ctrl.settings.AccessTokenURI = ctrl.settings.AccessTokenURI === '' ? provider.accessTokenUrl : ctrl.settings.AccessTokenURI;
ctrl.settings.ResourceURI = ctrl.settings.ResourceURI === '' ? provider.resourceUrl : ctrl.settings.ResourceURI;
ctrl.settings.UserIdentifier = ctrl.settings.UserIdentifier === '' ? provider.userIdentifier : ctrl.settings.UserIdentifier;
ctrl.settings.Scopes = ctrl.settings.Scopes === '' ? provider.scopes : ctrl.settings.Scopes;
if (provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') {
onMicrosoftTenantIDChange();
}
}
function onSelectProvider(provider, overrideConfiguration) {
ctrl.state.provider = provider;
if (overrideConfiguration) {
useDefaultProviderConfiguration();
} else {
useExistingConfiguration();
}
}
function onInit() {
if (ctrl.settings.RedirectURI === '') {
ctrl.settings.RedirectURI = window.location.origin;
}
function useDefaultProviderConfiguration() {
ctrl.settings.AuthorizationURI = ctrl.state.provider.authUrl;
ctrl.settings.AccessTokenURI = ctrl.state.provider.accessTokenUrl;
ctrl.settings.ResourceURI = ctrl.state.provider.resourceUrl;
ctrl.settings.UserIdentifier = ctrl.state.provider.userIdentifier;
ctrl.settings.Scopes = ctrl.state.provider.scopes;
if (ctrl.settings.AuthorizationURI !== '') {
ctrl.state.provider.authUrl = ctrl.settings.AuthorizationURI;
if (ctrl.state.provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') {
if (ctrl.settings.AuthorizationURI.indexOf('login.microsoftonline.com') > -1) {
var tenantID = ctrl.settings.AuthorizationURI.match(/login.microsoftonline.com\/(.*?)\//)[1];
ctrl.state.microsoftTenantID = tenantID;
onMicrosoftTenantIDChange();
}
}
function useExistingConfiguration() {
var provider = ctrl.state.provider;
ctrl.settings.AuthorizationURI = ctrl.settings.AuthorizationURI === '' ? provider.authUrl : ctrl.settings.AuthorizationURI;
ctrl.settings.AccessTokenURI = ctrl.settings.AccessTokenURI === '' ? provider.accessTokenUrl : ctrl.settings.AccessTokenURI;
ctrl.settings.ResourceURI = ctrl.settings.ResourceURI === '' ? provider.resourceUrl : ctrl.settings.ResourceURI;
ctrl.settings.UserIdentifier = ctrl.settings.UserIdentifier === '' ? provider.userIdentifier : ctrl.settings.UserIdentifier;
ctrl.settings.Scopes = ctrl.settings.Scopes === '' ? provider.scopes : ctrl.settings.Scopes;
if (provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') {
onMicrosoftTenantIDChange();
}
}
function onSelectProvider(provider, overrideConfiguration) {
ctrl.state.provider = provider;
if (overrideConfiguration) {
useDefaultProviderConfiguration();
} else {
useExistingConfiguration();
}
}
function onInit() {
if (ctrl.settings.RedirectURI === '') {
ctrl.settings.RedirectURI = window.location.origin;
}
if (ctrl.settings.AuthorizationURI !== '') {
ctrl.state.provider.authUrl = ctrl.settings.AuthorizationURI;
if (ctrl.settings.AuthorizationURI.indexOf('login.microsoftonline.com') > -1) {
var tenantID = ctrl.settings.AuthorizationURI.match(/login.microsoftonline.com\/(.*?)\//)[1];
ctrl.state.microsoftTenantID = tenantID;
onMicrosoftTenantIDChange();
}
}
}
});
}
});

View file

@ -4,22 +4,23 @@
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
With automatic user provisioning enabled, Portainer will create user(s) automatically with the standard user role. If
disabled, users must be created beforehand in Portainer in order to login.
With automatic user provisioning enabled, Portainer will create user(s) automatically with the standard user role. If disabled, users must be created beforehand in Portainer
in order to login.
</span>
</div>
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">Automatic user provisioning</label>
<label class="switch" style="margin-left: 20px">
<input type="checkbox" ng-model="$ctrl.settings.OAuthAutoCreateUsers" /><i></i>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.settings.OAuthAutoCreateUsers" /><i></i> </label>
</div>
<div ng-if="$ctrl.settings.OAuthAutoCreateUsers">
<div class="form-group">
<span class="col-sm-12 text-muted small">
<p>The users created by the automatic provisioning feature can be added to a default team on creation.</p>
<p>By assigning newly created users to a team, they will be able to access the environments associated to that team. This setting is optional and if not set, newly created users won't be able to access any environments.</p>
<p
>By assigning newly created users to a team, they will be able to access the environments associated to that team. This setting is optional and if not set, newly created
users won't be able to access any environments.</p
>
</span>
</div>
<div class="form-group">
@ -27,7 +28,9 @@
<span class="small text-muted" style="margin-left: 20px;" ng-if="$ctrl.teams.length === 0">
You have not yet created any teams. Head over to the <a ui-sref="portainer.teams">Teams view</a> to manage teams.
</span>
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.settings.DefaultTeamID = null" ng-disabled="!$ctrl.settings.DefaultTeamID" ng-if="$ctrl.teams.length > 0"><i class="fa fa-times" aria-hidden="true"></i></button>
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.settings.DefaultTeamID = null" ng-disabled="!$ctrl.settings.DefaultTeamID" ng-if="$ctrl.teams.length > 0"
><i class="fa fa-times" aria-hidden="true"></i
></button>
<div class="col-sm-9 col-lg-9" ng-if="$ctrl.teams.length > 0">
<select class="form-control" ng-model="$ctrl.settings.DefaultTeamID" ng-options="team.Id as team.Name for team in $ctrl.teams">
<option value="">No team</option>
@ -36,7 +39,7 @@
</div>
</div>
<oauth-providers-selector on-select="$ctrl.onSelectProvider" provider="$ctrl.state.provider"></oauth-providers-selector>
<oauth-providers-selector on-select="($ctrl.onSelectProvider)" provider="$ctrl.state.provider"></oauth-providers-selector>
<div class="col-sm-12 form-section-title">OAuth Configuration</div>
@ -47,12 +50,12 @@
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_microsoft_tenant_id"
placeholder="xxxxxxxxxxxxxxxxxxxx"
ng-model="$ctrl.state.microsoftTenantID"
ng-change="$ctrl.onMicrosoftTenantIDChange()"
type="text"
class="form-control"
id="oauth_microsoft_tenant_id"
placeholder="xxxxxxxxxxxxxxxxxxxx"
ng-model="$ctrl.state.microsoftTenantID"
ng-change="$ctrl.onMicrosoftTenantIDChange()"
/>
</div>
</div>
@ -63,13 +66,7 @@
<portainer-tooltip position="bottom" message="Public identifier of the OAuth application"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_client_id"
ng-model="$ctrl.settings.ClientID"
placeholder="xxxxxxxxxxxxxxxxxxxx"
/>
<input type="text" class="form-control" id="oauth_client_id" ng-model="$ctrl.settings.ClientID" placeholder="xxxxxxxxxxxxxxxxxxxx" />
</div>
</div>
@ -78,13 +75,7 @@
{{ $ctrl.state.provider.name == 'microsoft' ? 'Application key' : 'Client secret' }}
</label>
<div class="col-sm-9 col-lg-10">
<input
type="password"
class="form-control"
id="oauth_client_secret"
ng-model="$ctrl.settings.ClientSecret"
placeholder="xxxxxxxxxxxxxxxxxxxx"
/>
<input type="password" class="form-control" id="oauth_client_secret" ng-model="$ctrl.settings.ClientSecret" placeholder="xxxxxxxxxxxxxxxxxxxx" />
</div>
</div>
@ -92,56 +83,32 @@
<label for="oauth_authorization_uri" class="col-sm-3 col-lg-2 control-label text-left">
Authorization URL
<portainer-tooltip
position="bottom"
message="URL used to authenticate against the OAuth provider. Will redirect the user to the OAuth provider login view"
position="bottom"
message="URL used to authenticate against the OAuth provider. Will redirect the user to the OAuth provider login view"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_authorization_uri"
ng-model="$ctrl.settings.AuthorizationURI"
placeholder="https://example.com/oauth/authorize"
/>
<input type="text" class="form-control" id="oauth_authorization_uri" ng-model="$ctrl.settings.AuthorizationURI" placeholder="https://example.com/oauth/authorize" />
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_access_token_uri" class="col-sm-3 col-lg-2 control-label text-left">
Access token URL
<portainer-tooltip
position="bottom"
message="URL used by Portainer to exchange a valid OAuth authentication code for an access token"
></portainer-tooltip>
<portainer-tooltip position="bottom" message="URL used by Portainer to exchange a valid OAuth authentication code for an access token"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_access_token_uri"
ng-model="$ctrl.settings.AccessTokenURI"
placeholder="https://example.com/oauth/token"
/>
<input type="text" class="form-control" id="oauth_access_token_uri" ng-model="$ctrl.settings.AccessTokenURI" placeholder="https://example.com/oauth/token" />
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_resource_uri" class="col-sm-3 col-lg-2 control-label text-left">
Resource URL
<portainer-tooltip
position="bottom"
message="URL used by Portainer to retrieve information about the authenticated user"
></portainer-tooltip>
<portainer-tooltip position="bottom" message="URL used by Portainer to retrieve information about the authenticated user"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_resource_uri"
ng-model="$ctrl.settings.ResourceURI"
placeholder="https://example.com/user"
/>
<input type="text" class="form-control" id="oauth_resource_uri" ng-model="$ctrl.settings.ResourceURI" placeholder="https://example.com/user" />
</div>
</div>
@ -149,18 +116,12 @@
<label for="oauth_redirect_uri" class="col-sm-3 col-lg-2 control-label text-left">
Redirect URL
<portainer-tooltip
position="bottom"
message="URL used by the OAuth provider to redirect the user after successful authentication. Should be set to your Portainer instance URL"
position="bottom"
message="URL used by the OAuth provider to redirect the user after successful authentication. Should be set to your Portainer instance URL"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_redirect_uri"
ng-model="$ctrl.settings.RedirectURI"
placeholder="http://yourportainer.com/"
/>
<input type="text" class="form-control" id="oauth_redirect_uri" ng-model="$ctrl.settings.RedirectURI" placeholder="http://yourportainer.com/" />
</div>
</div>
@ -168,18 +129,12 @@
<label for="oauth_user_identifier" class="col-sm-3 col-lg-2 control-label text-left">
User identifier
<portainer-tooltip
position="bottom"
message="Identifier that will be used by Portainer to create an account for the authenticated user. Retrieved from the resource server specified via the Resource URL field"
position="bottom"
message="Identifier that will be used by Portainer to create an account for the authenticated user. Retrieved from the resource server specified via the Resource URL field"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_user_identifier"
ng-model="$ctrl.settings.UserIdentifier"
placeholder="id"
/>
<input type="text" class="form-control" id="oauth_user_identifier" ng-model="$ctrl.settings.UserIdentifier" placeholder="id" />
</div>
</div>
@ -187,18 +142,12 @@
<label for="oauth_scopes" class="col-sm-3 col-lg-2 control-label text-left">
Scopes
<portainer-tooltip
position="bottom"
message="Scopes required by the OAuth provider to retrieve information about the authenticated user. Refer to your OAuth provider documentation for more information about this"
position="bottom"
message="Scopes required by the OAuth provider to retrieve information about the authenticated user. Refer to your OAuth provider documentation for more information about this"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_scopes"
ng-model="$ctrl.settings.Scopes"
placeholder="id,email,name"
/>
<input type="text" class="form-control" id="oauth_scopes" ng-model="$ctrl.settings.Scopes" placeholder="id,email,name" />
</div>
</div>

View file

@ -2,7 +2,7 @@ angular.module('portainer.extensions.oauth').component('oauthSettings', {
templateUrl: './oauth-settings.html',
bindings: {
settings: '=',
teams: '<'
teams: '<',
},
controller: 'OAuthSettingsController'
controller: 'OAuthSettingsController',
});

View file

@ -1,13 +1,20 @@
angular.module('portainer.extensions.oauth')
.factory('OAuth', ['$resource', 'API_ENDPOINT_OAUTH', function OAuthFactory($resource, API_ENDPOINT_OAUTH) {
'use strict';
return $resource(API_ENDPOINT_OAUTH + '/:action', {}, {
validate: {
method: 'POST',
ignoreLoadingBar: true,
params: {
action: 'validate'
angular.module('portainer.extensions.oauth').factory('OAuth', [
'$resource',
'API_ENDPOINT_OAUTH',
function OAuthFactory($resource, API_ENDPOINT_OAUTH) {
'use strict';
return $resource(
API_ENDPOINT_OAUTH + '/:action',
{},
{
validate: {
method: 'POST',
ignoreLoadingBar: true,
params: {
action: 'validate',
},
},
}
}
});
}]);
);
},
]);

View file

@ -1,19 +1,23 @@
angular.module('portainer.extensions.rbac', ['ngResource'])
angular
.module('portainer.extensions.rbac', ['ngResource'])
.constant('API_ENDPOINT_ROLES', 'api/roles')
.config(['$stateRegistryProvider', function ($stateRegistryProvider) {
'use strict';
.config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
var roles = {
name: 'portainer.roles',
url: '/roles',
views: {
'content@': {
templateUrl: './views/roles/roles.html',
controller: 'RolesController',
controllerAs: 'ctrl'
}
}
};
var roles = {
name: 'portainer.roles',
url: '/roles',
views: {
'content@': {
templateUrl: './views/roles/roles.html',
controller: 'RolesController',
controllerAs: 'ctrl',
},
},
};
$stateRegistryProvider.register(roles);
}]);
$stateRegistryProvider.register(roles);
},
]);

View file

@ -1,44 +1,38 @@
<div class="col-sm-12" style="margin-bottom: 0px;">
<rd-widget ng-if="ctrl.users">
<rd-widget-header icon="fa-user-lock" title-text="Effective access viewer"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="col-sm-12 form-section-title">
User
</div>
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted" ng-if="ctrl.users.length === 0">
No user available
</span>
<ui-select ng-if="ctrl.users.length > 0" ng-model="ctrl.selectedUser" ng-change="ctrl.onUserSelect()">
<ui-select-match placeholder="Select a user">
<span>{{ $select.selected.Username }}</span>
</ui-select-match>
<ui-select-choices repeat="item in (ctrl.users | filter: $select.search)">
<span>{{ item.Username }}</span>
</ui-select-choices>
</ui-select>
</div>
<div class="col-sm-12" style="margin-bottom: 0px;">
<rd-widget ng-if="ctrl.users">
<rd-widget-header icon="fa-user-lock" title-text="Effective access viewer"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="col-sm-12 form-section-title">
User
</div>
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted" ng-if="ctrl.users.length === 0">
No user available
</span>
<ui-select ng-if="ctrl.users.length > 0" ng-model="ctrl.selectedUser" ng-change="ctrl.onUserSelect()">
<ui-select-match placeholder="Select a user">
<span>{{ $select.selected.Username }}</span>
</ui-select-match>
<ui-select-choices repeat="item in (ctrl.users | filter: $select.search)">
<span>{{ item.Username }}</span>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="col-sm-12 form-section-title">
Access
<div class="col-sm-12 form-section-title">
Access
</div>
<div>
<div class="small text-muted" style="margin-bottom: 15px;">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Effective role for each endpoint will be displayed for the selected user
</div>
<div>
<div class="small text-muted" style="margin-bottom: 15px;">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Effective role for each endpoint will be displayed for the selected user
</div>
</div>
<access-viewer-datatable
ng-if="ctrl.users"
table-key="access_viewer"
dataset="ctrl.userRoles"
order-by="EndpointName"
>
</access-viewer-datatable>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<access-viewer-datatable ng-if="ctrl.users" table-key="access_viewer" dataset="ctrl.userRoles" order-by="EndpointName"> </access-viewer-datatable>
</form>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -1,5 +1,5 @@
angular.module('portainer.app').component('accessViewer', {
templateUrl: './accessViewer.html',
controller: 'AccessViewerController',
controllerAs: 'ctrl'
controllerAs: 'ctrl',
});

View file

@ -20,9 +20,9 @@ class AccessViewerController {
this.userRoles = [];
const userRoles = {};
const user = this.selectedUser;
const userMemberships = _.filter(this.teamMemberships, {UserId: user.Id});
const userMemberships = _.filter(this.teamMemberships, { UserId: user.Id });
for (const [,endpoint] of _.entries(this.endpoints)) {
for (const [, endpoint] of _.entries(this.endpoints)) {
let role = this.getRoleFromUserEndpointPolicy(user, endpoint);
if (role) {
userRoles[endpoint.Id] = role;
@ -57,23 +57,23 @@ class AccessViewerController {
getRoleFromUserEndpointPolicy(user, endpoint) {
const policyRoles = [];
const policy = endpoint.UserAccessPolicies[user.Id];
if (policy) {
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, null, null);
policyRoles.push(accessPolicy);
}
if (policy) {
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, null, null);
policyRoles.push(accessPolicy);
}
return this.findLowestRole(policyRoles);
}
getRoleFromUserEndpointGroupPolicy(user, endpoint) {
const policyRoles = [];
const policy = this.groupUserAccessPolicies[endpoint.GroupId][user.Id];
if (policy) {
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, this.groups[endpoint.GroupId], null);
policyRoles.push(accessPolicy);
}
if (policy) {
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, this.groups[endpoint.GroupId], null);
policyRoles.push(accessPolicy);
}
return this.findLowestRole(policyRoles);
}
getRoleFromTeamEndpointPolicies(memberships, endpoint) {
const policyRoles = [];
for (const membership of memberships) {
@ -85,11 +85,11 @@ class AccessViewerController {
}
return this.findLowestRole(policyRoles);
}
getRoleFromTeamEndpointGroupPolicies(memberships, endpoint) {
const policyRoles = [];
for (const membership of memberships) {
const policy = this.groupTeamAccessPolicies[endpoint.GroupId][membership.TeamId]
const policy = this.groupTeamAccessPolicies[endpoint.GroupId][membership.TeamId];
if (policy) {
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, this.groups[endpoint.GroupId], this.teams[membership.TeamId]);
policyRoles.push(accessPolicy);
@ -107,7 +107,7 @@ class AccessViewerController {
const groups = await this.GroupService.groups();
this.groupUserAccessPolicies = {};
this.groupTeamAccessPolicies = {};
_.forEach(groups, group => {
_.forEach(groups, (group) => {
this.groupUserAccessPolicies[group.Id] = group.UserAccessPolicies;
this.groupTeamAccessPolicies[group.Id] = group.TeamAccessPolicies;
});
@ -117,12 +117,10 @@ class AccessViewerController {
this.teamMemberships = await this.TeamMembershipService.memberships();
}
} catch (err) {
this.Notifications.error("Failure", err, "Unable to retrieve accesses");
this.Notifications.error('Failure', err, 'Unable to retrieve accesses');
}
}
}
export default AccessViewerController;
angular
.module("portainer.app")
.controller("AccessViewerController", AccessViewerController);
angular.module('portainer.app').controller('AccessViewerController', AccessViewerController);

View file

@ -1,74 +1,73 @@
<div class="datatable">
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..." ng-model-options="{ debounce: 300 }">
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('EndpointName')">
Endpoint
<i class="fa fa-sort-alpha-down" aria-hidden="true"
ng-if="$ctrl.state.orderBy === 'EndpointName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true"
ng-if="$ctrl.state.orderBy === 'EndpointName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('RoleName')">
Role
<i class="fa fa-sort-alpha-down" aria-hidden="true"
ng-if="$ctrl.state.orderBy === 'RoleName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true"
ng-if="$ctrl.state.orderBy === 'RoleName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>Access origin</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)) track by $index">
<td>{{ item.EndpointName }}</td>
<td>{{ item.RoleName }}</td>
<td>{{ item.TeamName ? 'Team' : 'User'}} <code ng-if="item.TeamName">{{ item.TeamName }}</code> access defined on {{ item.AccessLocation }} <code ng-if="item.GroupName">{{item.GroupName}}</code>
<a ng-if="item.AccessLocation === 'endpoint'" ui-sref="portainer.endpoints.endpoint.access({id: item.EndpointId})"><i style="margin-left: 5px;" class="fa fa-users" aria-hidden="true"></i> Manage access </a>
<a ng-if="item.AccessLocation === 'endpoint group'" ui-sref="portainer.groups.group.access({id: item.GroupId})"><i style="margin-left: 5px;" class="fa fa-users" aria-hidden="true"></i> Manage access </a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Select a user to show associated access and role</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">The selected user does not have access to any endpoint(s)</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit"
ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." ng-model-options="{ debounce: 300 }" />
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('EndpointName')">
Endpoint
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'EndpointName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'EndpointName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('RoleName')">
Role
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>Access origin</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)) track by $index"
>
<td>{{ item.EndpointName }}</td>
<td>{{ item.RoleName }}</td>
<td
>{{ item.TeamName ? 'Team' : 'User' }} <code ng-if="item.TeamName">{{ item.TeamName }}</code> access defined on {{ item.AccessLocation }}
<code ng-if="item.GroupName">{{ item.GroupName }}</code>
<a ng-if="item.AccessLocation === 'endpoint'" ui-sref="portainer.endpoints.endpoint.access({id: item.EndpointId})"
><i style="margin-left: 5px;" class="fa fa-users" aria-hidden="true"></i> Manage access
</a>
<a ng-if="item.AccessLocation === 'endpoint group'" ui-sref="portainer.groups.group.access({id: item.GroupId})"
><i style="margin-left: 5px;" class="fa fa-users" aria-hidden="true"></i> Manage access
</a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Select a user to show associated access and role</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">The selected user does not have access to any endpoint(s)</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</div>

View file

@ -6,6 +6,6 @@ angular.module('portainer.app').component('accessViewerDatatable', {
titleIcon: '@',
tableKey: '@',
orderBy: '@',
dataset: '<'
}
dataset: '<',
},
});

View file

@ -1,14 +1,21 @@
<div class="datatable" ng-class="{'portainer-disabled-datatable': !$ctrl.rbacEnabled}">
<div class="datatable" ng-class="{ 'portainer-disabled-datatable': !$ctrl.rbacEnabled }">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
</div>
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus ng-disabled="!$ctrl.rbacEnabled" ng-model-options="{ debounce: 300 }">
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-disabled="!$ctrl.rbacEnabled"
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
@ -31,7 +38,10 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
ng-class="{ active: item.Checked }"
>
<td>{{ item.Name }}</td>
<td>{{ item.Description }}</td>
</tr>
@ -45,9 +55,7 @@
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">

View file

@ -8,6 +8,6 @@ angular.module('portainer.extensions.rbac').component('rolesDatatable', {
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
rbacEnabled: '<'
}
rbacEnabled: '<',
},
});

View file

@ -1,6 +1,8 @@
angular.module('portainer.extensions.rbac').directive('authorization', ['Authentication', 'ExtensionService', '$async',
function(Authentication, ExtensionService, $async) {
angular.module('portainer.extensions.rbac').directive('authorization', [
'Authentication',
'ExtensionService',
'$async',
function (Authentication, ExtensionService, $async) {
async function linkAsync(scope, elem, attrs) {
elem.hide();
try {
@ -13,7 +15,7 @@ angular.module('portainer.extensions.rbac').directive('authorization', ['Authent
elem.show();
return;
}
var authorizations = attrs.authorization.split(",");
var authorizations = attrs.authorization.split(',');
for (var i = 0; i < authorizations.length; i++) {
authorizations[i] = authorizations[i].trim();
}
@ -30,8 +32,9 @@ angular.module('portainer.extensions.rbac').directive('authorization', ['Authent
return {
restrict: 'A',
link: function(scope, elem, attrs) {
link: function (scope, elem, attrs) {
return $async(linkAsync, scope, elem, attrs);
}
}
}]);
},
};
},
]);

View file

@ -1,6 +1,8 @@
angular.module('portainer.extensions.rbac')
.directive('disableAuthorization', ['Authentication', 'ExtensionService', '$async', function(Authentication, ExtensionService, $async) {
angular.module('portainer.extensions.rbac').directive('disableAuthorization', [
'Authentication',
'ExtensionService',
'$async',
function (Authentication, ExtensionService, $async) {
async function linkAsync(scope, elem, attrs) {
try {
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
@ -11,7 +13,7 @@ angular.module('portainer.extensions.rbac')
return;
}
var authorizations = attrs.disableAuthorization.split(",");
var authorizations = attrs.disableAuthorization.split(',');
for (var i = 0; i < authorizations.length; i++) {
authorizations[i] = authorizations[i].trim();
}
@ -28,6 +30,7 @@ angular.module('portainer.extensions.rbac')
restrict: 'A',
link: function (scope, elem, attrs) {
return $async(linkAsync, scope, elem, attrs);
}
}
}]);
},
};
},
]);

View file

@ -5,11 +5,11 @@ export default function AccessViewerPolicyModel(policy, endpoint, roles, group,
this.RoleName = roles[policy.RoleId].Name;
if (group) {
this.GroupId = group.Id;
this.GroupName = group.Name
this.GroupName = group.Name;
}
if (team) {
this.TeamId = team.Id;
this.TeamName = team.Name;
}
this.AccessLocation = group ? 'endpoint group' : 'endpoint';
}
}

View file

@ -1,11 +1,18 @@
angular.module('portainer.app')
.factory('Roles', ['$resource', 'API_ENDPOINT_ROLES', function RolesFactory($resource, API_ENDPOINT_ROLES) {
'use strict';
return $resource(API_ENDPOINT_ROLES + '/:id', {}, {
create: { method: 'POST', ignoreLoadingBar: true },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } },
remove: { method: 'DELETE', params: { id: '@id'} }
});
}]);
angular.module('portainer.app').factory('Roles', [
'$resource',
'API_ENDPOINT_ROLES',
function RolesFactory($resource, API_ENDPOINT_ROLES) {
'use strict';
return $resource(
API_ENDPOINT_ROLES + '/:id',
{},
{
create: { method: 'POST', ignoreLoadingBar: true },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } },
remove: { method: 'DELETE', params: { id: '@id' } },
}
);
},
]);

View file

@ -4,44 +4,46 @@ import {
// EndpointRoleUpdateRequest
} from '../models/role';
angular.module('portainer.extensions.rbac')
.factory('RoleService', ['$q', 'Roles',
function RoleService($q, Roles) {
'use strict';
var service = {};
angular.module('portainer.extensions.rbac').factory('RoleService', [
'$q',
'Roles',
function RoleService($q, Roles) {
'use strict';
var service = {};
service.role = function(roleId) {
var deferred = $q.defer();
service.role = function (roleId) {
var deferred = $q.defer();
Roles.get({ id: roleId }).$promise
.then(function success(data) {
var role = new RoleViewModel(data);
deferred.resolve(role);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve role', err: err });
});
Roles.get({ id: roleId })
.$promise.then(function success(data) {
var role = new RoleViewModel(data);
deferred.resolve(role);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve role', err: err });
});
return deferred.promise;
};
return deferred.promise;
};
service.roles = function() {
return Roles.query({}).$promise;
};
service.roles = function () {
return Roles.query({}).$promise;
};
// service.createRole = function(model, endpoints) {
// var payload = new EndpointRoleCreateRequest(model, endpoints);
// return EndpointRoles.create(payload).$promise;
// };
//
// service.updateRole = function(model, endpoints) {
// var payload = new EndpointRoleUpdateRequest(model, endpoints);
// return EndpointRoles.update(payload).$promise;
// };
// service.createRole = function(model, endpoints) {
// var payload = new EndpointRoleCreateRequest(model, endpoints);
// return EndpointRoles.create(payload).$promise;
// };
//
// service.updateRole = function(model, endpoints) {
// var payload = new EndpointRoleUpdateRequest(model, endpoints);
// return EndpointRoles.update(payload).$promise;
// };
service.deleteRole = function(roleId) {
return Roles.remove({ id: roleId }).$promise;
};
service.deleteRole = function (roleId) {
return Roles.remove({ id: roleId }).$promise;
};
return service;
}]);
return service;
},
]);

View file

@ -11,26 +11,26 @@
<span class="small">
<p class="text-muted">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
The <a ui-sref="portainer.extensions.extension({id: 3})"
tooltip-append-to-body="true" tooltip-placement="bottom" tooltip-class="portainer-tooltip"
uib-tooltip="Feature available via an extension">Role-Based Access Control extension</a>
is required to use this feature.
The
<a
ui-sref="portainer.extensions.extension({id: 3})"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Feature available via an extension"
>Role-Based Access Control extension</a
>
is required to use this feature.
</p>
</span>
</information-panel>
<div class="row">
<div class="col-sm-12">
<roles-datatable
title-text="Roles" title-icon="fa-file-code"
dataset="ctrl.roles" table-key="roles"
order-by="Name"
rbac-enabled="ctrl.rbacEnabled"
></roles-datatable>
<roles-datatable title-text="Roles" title-icon="fa-file-code" dataset="ctrl.roles" table-key="roles" order-by="Name" rbac-enabled="ctrl.rbacEnabled"></roles-datatable>
</div>
</div>
<div class="row">
<access-viewer ng-if="ctrl.rbacEnabled">
</access-viewer>
</div>
<access-viewer ng-if="ctrl.rbacEnabled"> </access-viewer>
</div>

View file

@ -1,7 +1,6 @@
import angular from 'angular';
class RolesController {
/* @ngInject */
constructor(Notifications, RoleService, ExtensionService) {
this.Notifications = Notifications;

View file

@ -1,53 +1,55 @@
angular.module('portainer.extensions.registrymanagement', [])
.config(['$stateRegistryProvider', function ($stateRegistryProvider) {
'use strict';
angular.module('portainer.extensions.registrymanagement', []).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
var registryConfiguration = {
name: 'portainer.registries.registry.configure',
url: '/configure',
views: {
'content@': {
templateUrl: './views/configure/configureregistry.html',
controller: 'ConfigureRegistryController'
}
}
};
var registryConfiguration = {
name: 'portainer.registries.registry.configure',
url: '/configure',
views: {
'content@': {
templateUrl: './views/configure/configureregistry.html',
controller: 'ConfigureRegistryController',
},
},
};
var registryRepositories = {
name: 'portainer.registries.registry.repositories',
url: '/repositories',
views: {
'content@': {
templateUrl: './views/repositories/registryRepositories.html',
controller: 'RegistryRepositoriesController'
}
}
};
var registryRepositories = {
name: 'portainer.registries.registry.repositories',
url: '/repositories',
views: {
'content@': {
templateUrl: './views/repositories/registryRepositories.html',
controller: 'RegistryRepositoriesController',
},
},
};
var registryRepositoryTags = {
name: 'portainer.registries.registry.repository',
url: '/:repository',
views: {
'content@': {
templateUrl: './views/repositories/edit/registryRepository.html',
controller: 'RegistryRepositoryController'
}
}
};
var registryRepositoryTag = {
name: 'portainer.registries.registry.repository.tag',
url: '/:tag',
views: {
'content@': {
templateUrl: './views/repositories/tag/registryRepositoryTag.html',
controller: 'RegistryRepositoryTagController',
controllerAs: 'ctrl'
}
}
};
var registryRepositoryTags = {
name: 'portainer.registries.registry.repository',
url: '/:repository',
views: {
'content@': {
templateUrl: './views/repositories/edit/registryRepository.html',
controller: 'RegistryRepositoryController',
},
},
};
var registryRepositoryTag = {
name: 'portainer.registries.registry.repository.tag',
url: '/:tag',
views: {
'content@': {
templateUrl: './views/repositories/tag/registryRepositoryTag.html',
controller: 'RegistryRepositoryTagController',
controllerAs: 'ctrl',
},
},
};
$stateRegistryProvider.register(registryConfiguration);
$stateRegistryProvider.register(registryRepositories);
$stateRegistryProvider.register(registryRepositoryTags);
$stateRegistryProvider.register(registryRepositoryTag);
}]);
$stateRegistryProvider.register(registryConfiguration);
$stateRegistryProvider.register(registryRepositories);
$stateRegistryProvider.register(registryRepositoryTags);
$stateRegistryProvider.register(registryRepositoryTag);
},
]);

View file

@ -2,13 +2,19 @@
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
</div>
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-model-options="{ debounce: 300 }" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus>
<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 +28,18 @@
</a>
</th>
<th>
Tags count
Tags count
</th>
</tr>
</thead>
<tbody>
<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}">
<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})"
title="{{ item.Name }}">{{ item.Name }}</a>
<a ui-sref="portainer.registries.registry.repository({repository: item.Name})" title="{{ item.Name }}">{{ item.Name }}</a>
</td>
<td>{{ item.TagsCount }}</td>
</tr>
@ -45,9 +53,7 @@
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
@ -67,4 +73,4 @@
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -9,6 +9,6 @@ angular.module('portainer.extensions.registrymanagement').component('registryRep
orderBy: '@',
reverseOrder: '<',
paginationAction: '<',
loading: '<'
}
loading: '<',
},
});

View file

@ -1,8 +1,9 @@
import _ from 'lodash-es';
angular.module('portainer.app')
.controller('RegistryRepositoriesDatatableController', ['$scope', '$controller',
function($scope, $controller) {
angular.module('portainer.app').controller('RegistryRepositoriesDatatableController', [
'$scope',
'$controller',
function ($scope, $controller) {
var ctrl = this;
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
@ -12,16 +13,29 @@ angular.module('portainer.app')
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();
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) {
$scope.$watch(
function () {
return ctrl.state.filteredDataSet;
},
function (newValue, oldValue) {
if (newValue && areDifferent(oldValue, newValue)) {
ctrl.paginationAction(_.filter(newValue, {'TagsCount':0}));
ctrl.paginationAction(_.filter(newValue, { TagsCount: 0 }));
}
}, true);
}
},
true
);
},
]);

View file

@ -2,9 +2,7 @@
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
</div>
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
</div>
<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)">
@ -13,7 +11,15 @@
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-model-options="{ debounce: 300 }" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus>
<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">
@ -37,15 +43,17 @@
</tr>
</thead>
<tbody>
<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}">
<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">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="portainer.registries.registry.repository.tag({tag: item.Name})"
title="{{ item.Name }}">{{ item.Name }}</a>
<a ui-sref="portainer.registries.registry.repository.tag({tag: item.Name})" title="{{ item.Name }}">{{ item.Name }}</a>
</td>
<td>{{ item.Os }}/{{ item.Architecture }}</td>
<td>{{ item.ImageId | trimshasum }}</td>
@ -57,9 +65,11 @@
</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();" />
<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>
<a class="interactive" ng-click="$ctrl.retagAction(item); $event.stopPropagation();"><i class="fa fa-check-square"></i></a>
</span>
@ -75,9 +85,7 @@
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
@ -97,4 +105,4 @@
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -12,6 +12,6 @@ angular.module('portainer.extensions.registrymanagement').component('registriesR
retagAction: '<',
advancedFeaturesAvailable: '<',
paginationAction: '<',
loading: '<'
}
loading: '<',
},
});

View file

@ -1,8 +1,9 @@
import _ from 'lodash-es';
angular.module('portainer.app')
.controller('RegistryRepositoriesTagsDatatableController', ['$scope', '$controller',
function($scope, $controller) {
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;
@ -20,12 +21,17 @@ angular.module('portainer.app')
return namesA.join(',') !== namesB.join(',');
}
$scope.$watch(function() { return ctrl.state.filteredDataSet;},
function(newValue, oldValue) {
$scope.$watch(
function () {
return ctrl.state.filteredDataSet;
},
function (newValue, oldValue) {
if (newValue && newValue.length && areDifferent(oldValue, newValue)) {
ctrl.paginationAction(_.filter(newValue, {'ImageId': ''}));
ctrl.paginationAction(_.filter(newValue, { ImageId: '' }));
ctrl.resetSelectionState();
}
}, true);
}
},
true
);
},
]);

View file

@ -1,8 +1,8 @@
import _ from 'lodash-es';
import { RepositoryTagViewModel } from '../models/repositoryTag';
angular.module('portainer.extensions.registrymanagement')
.factory('RegistryV2Helper', [function RegistryV2HelperFactory() {
angular.module('portainer.extensions.registrymanagement').factory('RegistryV2Helper', [
function RegistryV2HelperFactory() {
'use strict';
var helper = {};
@ -21,7 +21,7 @@ angular.module('portainer.extensions.registrymanagement')
var arch = v1.architecture;
var size = v2.layers.reduce(function (a, b) {
return {
size: a.size + b.size
size: a.size + b.size,
};
}).size;
var imageId = v2.config.digest;
@ -31,4 +31,5 @@ angular.module('portainer.extensions.registrymanagement')
};
return helper;
}]);
},
]);

View file

@ -5,4 +5,4 @@ export function RegistryImageLayerViewModel(order, data) {
this.Id = data.id;
this.Created = data.created;
this.CreatedBy = _.join(data.container_config.Cmd, ' ');
}
}

View file

@ -12,4 +12,4 @@ export function RegistryRepositoryViewModel(item) {
export function RegistryRepositoryGitlabViewModel(data) {
this.Name = data.path;
this.TagsCount = data.tags.length;
}
}

View file

@ -1,6 +1,6 @@
export const RegistryTypes = Object.freeze({
'QUAY': 1,
'AZURE': 2,
'CUSTOM': 3,
'GITLAB': 4
})
QUAY: 1,
AZURE: 2,
CUSTOM: 3,
GITLAB: 4,
});

View file

@ -1,22 +1,22 @@
export function RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2, history) {
this.Name = name;
this.Os = os || '';
this.Architecture = arch || '';
this.Size = size || 0;
this.ImageDigest = imageDigest || '';
this.ImageId = imageId || '';
this.ManifestV2 = v2 || {};
this.History = history || [];
this.Name = name;
this.Os = os || '';
this.Architecture = arch || '';
this.Size = size || 0;
this.ImageDigest = imageDigest || '';
this.ImageId = imageId || '';
this.ManifestV2 = v2 || {};
this.History = history || [];
}
export function RepositoryShortTag(name, imageId, imageDigest, manifest) {
this.Name = name;
this.ImageId = imageId;
this.ImageDigest = imageDigest;
this.ManifestV2 = manifest;
this.Name = name;
this.ImageId = imageId;
this.ImageDigest = imageDigest;
this.ManifestV2 = manifest;
}
export function RepositoryAddTagPayload(tag, manifest) {
this.Tag = tag;
this.Manifest = manifest;
this.Tag = tag;
this.Manifest = manifest;
}

View file

@ -1,27 +1,34 @@
import linkGetResponse from './transform/linkGetResponse'
import linkGetResponse from './transform/linkGetResponse';
angular.module('portainer.extensions.registrymanagement')
.factory('RegistryCatalog', ['$resource', 'API_ENDPOINT_REGISTRIES',
function RegistryCatalogFactory($resource, API_ENDPOINT_REGISTRIES) {
'use strict';
return $resource(API_ENDPOINT_REGISTRIES + '/:id/v2/:action', {},
{
get: {
method: 'GET',
params: { id: '@id', action: '_catalog' },
transformResponse: linkGetResponse
},
ping: {
method: 'GET',
params: { id: '@id' }, timeout: 3500
},
pingWithForceNew: {
method: 'GET',
params: { id: '@id' }, timeout: 3500,
headers: { 'X-RegistryManagement-ForceNew': '1' }
}
angular.module('portainer.extensions.registrymanagement').factory('RegistryCatalog', [
'$resource',
'API_ENDPOINT_REGISTRIES',
function RegistryCatalogFactory($resource, API_ENDPOINT_REGISTRIES) {
'use strict';
return $resource(
API_ENDPOINT_REGISTRIES + '/:id/v2/:action',
{},
{
get: {
method: 'GET',
params: { id: '@id', action: '_catalog' },
transformResponse: linkGetResponse,
},
ping: {
method: 'GET',
params: { id: '@id' },
timeout: 3500,
},
pingWithForceNew: {
method: 'GET',
params: { id: '@id' },
timeout: 3500,
headers: { 'X-RegistryManagement-ForceNew': '1' },
},
},
{
stripTrailingSlashes: false,
}
);
},
{
stripTrailingSlashes: false
});
}]);
]);

View file

@ -1,33 +1,38 @@
import gitlabResponseGetLink from './transform/gitlabResponseGetLink'
import gitlabResponseGetLink from './transform/gitlabResponseGetLink';
angular.module('portainer.extensions.registrymanagement')
.factory('Gitlab', ['$resource', 'API_ENDPOINT_REGISTRIES',
function GitlabFactory($resource, API_ENDPOINT_REGISTRIES) {
'use strict';
return function(env) {
const headers = {};
if (env) {
headers['Private-Token'] = env.token;
headers['X-Gitlab-Domain'] = env.url
}
const baseUrl = API_ENDPOINT_REGISTRIES + '/:id/proxies/gitlab/api/v4/projects';
return $resource(baseUrl, {id:'@id'},
{
projects: {
method: 'GET',
params: { membership: 'true' },
transformResponse: gitlabResponseGetLink,
headers: headers
},
repositories :{
method: 'GET',
url: baseUrl + '/:projectId/registry/repositories',
params: { tags: true },
headers: headers,
transformResponse: gitlabResponseGetLink
angular.module('portainer.extensions.registrymanagement').factory('Gitlab', [
'$resource',
'API_ENDPOINT_REGISTRIES',
function GitlabFactory($resource, API_ENDPOINT_REGISTRIES) {
'use strict';
return function (env) {
const headers = {};
if (env) {
headers['Private-Token'] = env.token;
headers['X-Gitlab-Domain'] = env.url;
}
});
};
}]);
const baseUrl = API_ENDPOINT_REGISTRIES + '/:id/proxies/gitlab/api/v4/projects';
return $resource(
baseUrl,
{ id: '@id' },
{
projects: {
method: 'GET',
params: { membership: 'true' },
transformResponse: gitlabResponseGetLink,
headers: headers,
},
repositories: {
method: 'GET',
url: baseUrl + '/:projectId/registry/repositories',
params: { tags: true },
headers: headers,
transformResponse: gitlabResponseGetLink,
},
}
);
};
},
]);

View file

@ -6,84 +6,85 @@
import $ from 'jquery';
angular.module('portainer.extensions.registrymanagement')
.factory('RegistryManifestsJquery', ['API_ENDPOINT_REGISTRIES',
function RegistryManifestsJqueryFactory(API_ENDPOINT_REGISTRIES) {
'use strict';
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);
function buildUrl(params) {
return API_ENDPOINT_REGISTRIES + '/' + params.id + '/v2/' + params.repository + '/manifests/' + params.tag;
}
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)
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),
});
});
})
}
}
return {
get: _get,
getV2: _getV2,
put: _put,
delete: _delete
}
}]);
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,13 +1,20 @@
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' },
transformResponse: 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' },
transformResponse: linkGetResponse,
},
}
);
},
]);

View file

@ -7,4 +7,4 @@ export default function gitlabResponseGetLink(data, headers) {
response = data;
}
return response;
}
}

View file

@ -10,4 +10,4 @@ export default function linkGetResponse(data, headers) {
}
}
return response;
}
}

View file

@ -2,85 +2,86 @@ import _ from 'lodash-es';
import { RegistryGitlabProject } from '../models/gitlabRegistry';
import { RegistryRepositoryGitlabViewModel } from '../models/registryRepository';
angular.module('portainer.extensions.registrymanagement')
.factory('RegistryGitlabService', ['$async', 'Gitlab',
function RegistryGitlabServiceFactory($async, Gitlab) {
'use strict';
var service = {};
angular.module('portainer.extensions.registrymanagement').factory('RegistryGitlabService', [
'$async',
'Gitlab',
function RegistryGitlabServiceFactory($async, Gitlab) {
'use strict';
var service = {};
/**
* PROJECTS
*/
/**
* PROJECTS
*/
async function _getProjectsPage(env, params, projects) {
const response = await Gitlab(env).projects(params).$promise;
projects = _.concat(projects, response.data);
if (response.next) {
params.page = response.next;
projects = await _getProjectsPage(env, params, projects);
async function _getProjectsPage(env, params, projects) {
const response = await Gitlab(env).projects(params).$promise;
projects = _.concat(projects, response.data);
if (response.next) {
params.page = response.next;
projects = await _getProjectsPage(env, params, projects);
}
return projects;
}
return projects;
}
async function projectsAsync(url, token) {
try {
const data = await _getProjectsPage({url: url, token: token}, {page: 1}, []);
return _.map(data, (project) => new RegistryGitlabProject(project));
} catch (error) {
throw {msg: 'Unable to retrieve projects', err: error};
async function projectsAsync(url, token) {
try {
const data = await _getProjectsPage({ url: url, token: token }, { page: 1 }, []);
return _.map(data, (project) => new RegistryGitlabProject(project));
} catch (error) {
throw { msg: 'Unable to retrieve projects', err: error };
}
}
}
/**
* END PROJECTS
*/
/**
* END PROJECTS
*/
/**
* REPOSITORIES
*/
/**
* REPOSITORIES
*/
async function _getRepositoriesPage(params, repositories) {
const response = await Gitlab().repositories(params).$promise;
repositories = _.concat(repositories, response.data);
if (response.next) {
params.page = response.next;
repositories = await _getRepositoriesPage(params, repositories);
async function _getRepositoriesPage(params, repositories) {
const response = await Gitlab().repositories(params).$promise;
repositories = _.concat(repositories, response.data);
if (response.next) {
params.page = response.next;
repositories = await _getRepositoriesPage(params, repositories);
}
return repositories;
}
return repositories;
}
async function repositoriesAsync(registry) {
try {
const params = {
id: registry.Id,
projectId: registry.Gitlab.ProjectId,
page: 1
};
const data = await _getRepositoriesPage(params, []);
return _.map(data, (r) => new RegistryRepositoryGitlabViewModel(r));
} catch (error) {
throw {msg: 'Unable to retrieve repositories', err: error};
async function repositoriesAsync(registry) {
try {
const params = {
id: registry.Id,
projectId: registry.Gitlab.ProjectId,
page: 1,
};
const data = await _getRepositoriesPage(params, []);
return _.map(data, (r) => new RegistryRepositoryGitlabViewModel(r));
} catch (error) {
throw { msg: 'Unable to retrieve repositories', err: error };
}
}
}
/**
* END REPOSITORIES
*/
/**
* END REPOSITORIES
*/
/**
* SERVICE FUNCTIONS DECLARATION
*/
/**
* SERVICE FUNCTIONS DECLARATION
*/
function projects(url, token) {
return $async(projectsAsync, url, token);
}
function projects(url, token) {
return $async(projectsAsync, url, token);
}
function repositories(registry) {
return $async(repositoriesAsync, registry);
}
function repositories(registry) {
return $async(repositoriesAsync, registry);
}
service.projects = projects;
service.repositories = repositories;
return service;
}
service.projects = projects;
service.repositories = repositories;
return service;
},
]);

View file

@ -1,82 +1,84 @@
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
angular.module('portainer.extensions.registrymanagement')
.factory('RegistryServiceSelector', ['$q', 'RegistryV2Service', 'RegistryGitlabService',
function RegistryServiceSelector($q, RegistryV2Service, RegistryGitlabService) {
'use strict';
const service = {};
angular.module('portainer.extensions.registrymanagement').factory('RegistryServiceSelector', [
'$q',
'RegistryV2Service',
'RegistryGitlabService',
function RegistryServiceSelector($q, RegistryV2Service, RegistryGitlabService) {
'use strict';
const service = {};
service.ping = ping;
service.repositories = repositories;
service.getRepositoriesDetails = getRepositoriesDetails;
service.tags = tags;
service.getTagsDetails = getTagsDetails;
service.tag = tag;
service.addTag = addTag;
service.deleteManifest = deleteManifest;
service.ping = ping;
service.repositories = repositories;
service.getRepositoriesDetails = getRepositoriesDetails;
service.tags = tags;
service.getTagsDetails = getTagsDetails;
service.tag = tag;
service.addTag = addTag;
service.deleteManifest = deleteManifest;
service.shortTagsWithProgress = shortTagsWithProgress;
service.deleteTagsWithProgress = deleteTagsWithProgress;
service.retagWithProgress = retagWithProgress;
service.shortTagsWithProgress = shortTagsWithProgress;
service.deleteTagsWithProgress = deleteTagsWithProgress;
service.retagWithProgress = retagWithProgress;
function ping(registry, forceNewConfig) {
let service = RegistryV2Service;
return service.ping(registry, forceNewConfig)
}
function repositories(registry) {
let service = RegistryV2Service;
if (registry.Type === RegistryTypes.GITLAB) {
service = RegistryGitlabService;
function ping(registry, forceNewConfig) {
let service = RegistryV2Service;
return service.ping(registry, forceNewConfig);
}
return service.repositories(registry);
}
function getRepositoriesDetails(registry, repositories) {
let service = RegistryV2Service;
return service.getRepositoriesDetails(registry, repositories);
}
function repositories(registry) {
let service = RegistryV2Service;
if (registry.Type === RegistryTypes.GITLAB) {
service = RegistryGitlabService;
}
return service.repositories(registry);
}
function tags(registry, repository) {
let service = RegistryV2Service;
return service.tags(registry, repository);
}
function getRepositoriesDetails(registry, repositories) {
let service = RegistryV2Service;
return service.getRepositoriesDetails(registry, repositories);
}
function getTagsDetails(registry, repository, tags) {
let service = RegistryV2Service;
return service.getTagsDetails(registry, repository, tags);
}
function tags(registry, repository) {
let service = RegistryV2Service;
return service.tags(registry, repository);
}
function tag(registry, repository, tag) {
let service = RegistryV2Service;
return service.tag(registry, repository, tag);
}
function getTagsDetails(registry, repository, tags) {
let service = RegistryV2Service;
return service.getTagsDetails(registry, repository, tags);
}
function addTag(registry, repository, tag, manifest) {
let service = RegistryV2Service;
return service.addTag(registry, repository, tag, manifest);
}
function tag(registry, repository, tag) {
let service = RegistryV2Service;
return service.tag(registry, repository, tag);
}
function deleteManifest(registry, repository, digest) {
let service = RegistryV2Service;
return service.deleteManifest(registry, repository, digest);
}
function addTag(registry, repository, tag, manifest) {
let service = RegistryV2Service;
return service.addTag(registry, repository, tag, manifest);
}
function shortTagsWithProgress(registry, repository, tagsList) {
let service = RegistryV2Service;
return service.shortTagsWithProgress(registry, repository, tagsList);
}
function deleteManifest(registry, repository, digest) {
let service = RegistryV2Service;
return service.deleteManifest(registry, repository, digest);
}
function deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags) {
let service = RegistryV2Service;
return service.deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags);
}
function shortTagsWithProgress(registry, repository, tagsList) {
let service = RegistryV2Service;
return service.shortTagsWithProgress(registry, repository, tagsList);
}
function retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags) {
let service = RegistryV2Service;
return service.retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags);
}
function deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags) {
let service = RegistryV2Service;
return service.deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags);
}
return service;
}
function retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags) {
let service = RegistryV2Service;
return service.retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags);
}
return service;
},
]);

View file

@ -1,299 +1,311 @@
import _ from 'lodash-es';
import { RepositoryShortTag } from '../models/repositoryTag';
import { RepositoryAddTagPayload } from '../models/repositoryTag'
import { RepositoryAddTagPayload } 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 = {};
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 = {};
/**
* PING
*/
function ping(registry, forceNewConfig) {
const id = registry.Id;
if (forceNewConfig) {
return RegistryCatalog.pingWithForceNew({ id: id }).$promise;
/**
* PING
*/
function ping(registry, forceNewConfig) {
const id = registry.Id;
if (forceNewConfig) {
return RegistryCatalog.pingWithForceNew({ id: id }).$promise;
}
return RegistryCatalog.ping({ id: id }).$promise;
}
return RegistryCatalog.ping({ id: id }).$promise;
}
/**
* END PING
*/
/**
* END PING
*/
/**
* REPOSITORIES
*/
/**
* REPOSITORIES
*/
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 _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;
}
function repositories(registry) {
const deferred = $q.defer();
const id = registry.Id;
_getCatalog(id)
.then(function success(data) {
const repositories = _.map(data, (repositoryName) => new RegistryRepositoryViewModel(repositoryName));
deferred.resolve(repositories);
})
.catch(function error(err) {
deferred.reject({
msg: 'Unable to retrieve repositories',
err: err,
});
});
return deferred.promise;
}
function getRepositoriesDetails(registry, repositories) {
const deferred = $q.defer();
const promises = _.map(repositories, (repository) => tags(registry, repository.Name));
$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;
}
/**
* END REPOSITORIES
*/
/**
* TAGS
*/
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,
});
});
}
function tags(registry, repository) {
const deferred = $q.defer();
const id = registry.Id;
_getTagsPage({ id: id, repository: repository }, deferred, { tags: [] });
return deferred.promise;
}
function getTagsDetails(registry, repository, tags) {
const promises = _.map(tags, (t) => tag(registry, repository, t.Name));
return $q.all(promises);
}
function tag(registry, repository, tag) {
const deferred = $q.defer();
const id = registry.Id;
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;
}
/**
* END TAGS
*/
/**
* ADD TAG
*/
// tag: RepositoryAddTagPayload
function _addTagFromGenerator(registry, repository, tag) {
return addTag(registry, repository, tag.Tag, tag.Manifest);
}
function addTag(registry, repository, tag, manifest) {
const id = registry.Id;
delete manifest.digest;
return RegistryManifestsJquery.put(
{
id: id,
repository: repository,
tag: tag,
},
manifest
);
}
async function* _addTagsWithProgress(registry, repository, tagsList, progression = 0) {
for await (const partialResult of genericAsyncGenerator($q, tagsList, _addTagFromGenerator, [registry, repository])) {
if (typeof partialResult === 'number') {
yield progression + partialResult;
} else {
yield partialResult;
}
}
});
}
}
function _getCatalog(id) {
var deferred = $q.defer();
var repositories = [];
/**
* END ADD TAG
*/
_getCatalogPage({id: id}, deferred, repositories);
return deferred.promise;
}
/**
* DELETE MANIFEST
*/
function repositories(registry) {
const deferred = $q.defer();
const id = registry.Id;
_getCatalog(id).then(function success(data) {
const repositories = _.map(data, (repositoryName) => new RegistryRepositoryViewModel(repositoryName));
deferred.resolve(repositories);
})
.catch(function error(err) {
deferred.reject({
msg: 'Unable to retrieve repositories',
err: err
});
});
return deferred.promise;
}
function getRepositoriesDetails(registry, repositories) {
const deferred = $q.defer();
const promises = _.map(repositories, (repository) => tags(registry, repository.Name));
$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;
}
/**
* END REPOSITORIES
*/
/**
* TAGS
*/
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
});
});
}
function tags(registry, repository) {
const deferred = $q.defer();
const id = registry.Id;
_getTagsPage({id: id, repository: repository}, deferred, {tags:[]});
return deferred.promise;
}
function getTagsDetails(registry, repository, tags) {
const promises = _.map(tags, (t) => tag(registry, repository, t.Name));
return $q.all(promises);
}
function tag(registry, repository, tag) {
const deferred = $q.defer();
const id = registry.Id;
var promises = {
v1: RegistryManifestsJquery.get({
function deleteManifest(registry, repository, imageDigest) {
const id = registry.Id;
return RegistryManifestsJquery.delete({
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
tag: imageDigest,
});
});
}
return deferred.promise;
}
/**
* END TAGS
*/
/**
* ADD TAG
*/
// tag: RepositoryAddTagPayload
function _addTagFromGenerator(registry, repository, tag) {
return addTag(registry, repository, tag.Tag, tag.Manifest);
}
function addTag(registry, repository, tag, manifest) {
const id = registry.Id;
delete manifest.digest;
return RegistryManifestsJquery.put({
id: id,
repository: repository,
tag: tag
}, manifest);
}
async function* _addTagsWithProgress(registry, repository, tagsList, progression = 0) {
for await (const partialResult of genericAsyncGenerator($q, tagsList, _addTagFromGenerator, [registry, repository])) {
if (typeof partialResult === 'number') {
yield progression + partialResult;
} else {
async function* _deleteManifestsWithProgress(registry, repository, manifests) {
for await (const partialResult of genericAsyncGenerator($q, manifests, deleteManifest, [registry, repository])) {
yield partialResult;
}
}
}
/**
* END ADD TAG
*/
/**
* END DELETE MANIFEST
*/
/**
* DELETE MANIFEST
*/
/**
* SHORT TAG
*/
function deleteManifest(registry, repository, imageDigest) {
const id = registry.Id;
return RegistryManifestsJquery.delete({
id: id,
repository: repository,
tag: imageDigest
});
}
async function* _deleteManifestsWithProgress(registry, repository, manifests) {
for await (const partialResult of genericAsyncGenerator($q, manifests, deleteManifest, [registry, repository])) {
yield partialResult;
function _shortTagFromGenerator(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));
});
}
}
/**
* END DELETE MANIFEST
*/
async function* shortTagsWithProgress(registry, repository, tagsList) {
const id = registry.Id;
yield* genericAsyncGenerator($q, tagsList, _shortTagFromGenerator, [id, repository]);
}
/**
* SHORT TAG
*/
/**
* END SHORT TAG
*/
function _shortTagFromGenerator(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))
});
}
/**
* RETAG
*/
async function* retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags) {
yield* _deleteManifestsWithProgress(registry, repository, modifiedDigests);
async function* shortTagsWithProgress(registry, repository, tagsList) {
const id = registry.Id;
yield* genericAsyncGenerator($q, tagsList, _shortTagFromGenerator, [id, repository]);
}
const newTags = _.map(impactedTags, (item) => {
const tagFromTable = _.find(modifiedTags, { Name: item.Name });
const name = tagFromTable && tagFromTable.Name !== tagFromTable.NewName ? tagFromTable.NewName : item.Name;
return new RepositoryAddTagPayload(name, item.ManifestV2);
});
/**
* END SHORT TAG
*/
yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length);
}
/**
* RETAG
*/
async function* retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags){
yield* _deleteManifestsWithProgress(registry, repository, modifiedDigests);
/**
* END RETAG
*/
const newTags = _.map(impactedTags, (item) => {
const tagFromTable = _.find(modifiedTags, { 'Name': item.Name });
const name = tagFromTable && tagFromTable.Name !== tagFromTable.NewName ? tagFromTable.NewName : item.Name;
return new RepositoryAddTagPayload(name, item.ManifestV2);
});
/**
* DELETE TAGS
*/
yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length);
}
async function* deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags) {
yield* _deleteManifestsWithProgress(registry, repository, modifiedDigests);
/**
* END RETAG
*/
const newTags = _.map(impactedTags, (item) => new RepositoryAddTagPayload(item.Name, item.ManifestV2));
/**
* DELETE TAGS
*/
yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length);
}
async function* deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags) {
yield* _deleteManifestsWithProgress(registry, repository, modifiedDigests);
/**
* END DELETE TAGS
*/
const newTags = _.map(impactedTags, (item) => new RepositoryAddTagPayload(item.Name, item.ManifestV2));
/**
* SERVICE FUNCTIONS DECLARATION
*/
yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length);
}
service.ping = ping;
/**
* END DELETE TAGS
*/
service.repositories = repositories;
service.getRepositoriesDetails = getRepositoriesDetails;
/**
* SERVICE FUNCTIONS DECLARATION
*/
service.tags = tags;
service.tag = tag;
service.getTagsDetails = getTagsDetails;
service.ping = ping;
service.shortTagsWithProgress = shortTagsWithProgress;
service.repositories = repositories;
service.getRepositoriesDetails = getRepositoriesDetails;
service.addTag = addTag;
service.deleteManifest = deleteManifest;
service.tags = tags;
service.tag = tag;
service.getTagsDetails = getTagsDetails;
service.deleteTagsWithProgress = deleteTagsWithProgress;
service.retagWithProgress = retagWithProgress;
service.shortTagsWithProgress = shortTagsWithProgress;
service.addTag = addTag;
service.deleteManifest = deleteManifest;
service.deleteTagsWithProgress = deleteTagsWithProgress;
service.retagWithProgress = retagWithProgress;
return service;
}
return service;
},
]);

View file

@ -1,70 +1,75 @@
import { RegistryManagementConfigurationDefaultModel } from '../../../../portainer/models/registry';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
angular.module('portainer.extensions.registrymanagement')
.controller('ConfigureRegistryController', ['$scope', '$state', '$transition$', 'RegistryService', 'RegistryServiceSelector', 'Notifications',
function ($scope, $state, $transition$, RegistryService, RegistryServiceSelector, Notifications) {
angular.module('portainer.extensions.registrymanagement').controller('ConfigureRegistryController', [
'$scope',
'$state',
'$transition$',
'RegistryService',
'RegistryServiceSelector',
'Notifications',
function ($scope, $state, $transition$, RegistryService, RegistryServiceSelector, Notifications) {
$scope.state = {
testInProgress: false,
updateInProgress: false,
validConfiguration: false,
};
$scope.state = {
testInProgress: false,
updateInProgress: false,
validConfiguration : false
};
$scope.testConfiguration = testConfiguration;
$scope.updateConfiguration = updateConfiguration;
$scope.testConfiguration = testConfiguration;
$scope.updateConfiguration = updateConfiguration;
function testConfiguration() {
$scope.state.testInProgress = true;
function testConfiguration() {
$scope.state.testInProgress = true;
RegistryService.configureRegistry($scope.registry.Id, $scope.model)
.then(function success() {
return RegistryServiceSelector.ping($scope.registry, true);
})
.then(function success() {
Notifications.success('Success', 'Valid management configuration');
$scope.state.validConfiguration = true;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Invalid management configuration');
})
.finally(function final() {
$scope.state.testInProgress = false;
});
}
RegistryService.configureRegistry($scope.registry.Id, $scope.model)
.then(function success() {
return RegistryServiceSelector.ping($scope.registry, true);
})
.then(function success() {
Notifications.success('Success', 'Valid management configuration');
$scope.state.validConfiguration = true;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Invalid management configuration');
})
.finally(function final() {
$scope.state.testInProgress = false;
});
}
function updateConfiguration() {
$scope.state.updateInProgress = true;
function updateConfiguration() {
$scope.state.updateInProgress = true;
RegistryService.configureRegistry($scope.registry.Id, $scope.model)
.then(function success() {
Notifications.success('Success', 'Registry management configuration updated');
$state.go('portainer.registries.registry.repositories', { id: $scope.registry.Id }, { reload: true });
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update registry management configuration');
})
.finally(function final() {
$scope.state.updateInProgress = false;
});
}
RegistryService.configureRegistry($scope.registry.Id, $scope.model)
.then(function success() {
Notifications.success('Success', 'Registry management configuration updated');
$state.go('portainer.registries.registry.repositories', { id: $scope.registry.Id }, {reload: true});
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update registry management configuration');
})
.finally(function final() {
$scope.state.updateInProgress = false;
});
}
function initView() {
var registryId = $transition$.params().id;
$scope.RegistryTypes = RegistryTypes;
function initView() {
var registryId = $transition$.params().id;
$scope.RegistryTypes = RegistryTypes;
RegistryService.registry(registryId)
.then(function success(data) {
var registry = data;
var model = new RegistryManagementConfigurationDefaultModel(registry);
RegistryService.registry(registryId)
.then(function success(data) {
var registry = data;
var model = new RegistryManagementConfigurationDefaultModel(registry);
$scope.registry = registry;
$scope.model = model;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registry details');
});
}
$scope.registry = registry;
$scope.model = model;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registry details');
});
}
initView();
}]);
initView();
},
]);

View file

@ -15,7 +15,8 @@
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
The following configuration will be used to access this <a href="https://docs.docker.com/registry/spec/api/" target="_blank">registry API</a> to provide Portainer management features.
The following configuration will be used to access this <a href="https://docs.docker.com/registry/spec/api/" target="_blank">registry API</a> to provide Portainer
management features.
</span>
</div>
<div class="col-sm-12 form-section-title">
@ -27,7 +28,7 @@
Registry URL
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_url" ng-model="registry.URL" disabled>
<input type="text" class="form-control" id="registry_url" ng-model="registry.URL" disabled />
</div>
</div>
<!-- !registry-url-input -->
@ -38,9 +39,7 @@
Authentication
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to this registry."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="model.Authentication"><i></i>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="model.Authentication" /><i></i> </label>
</div>
</div>
<!-- !authentication-checkbox -->
@ -50,7 +49,7 @@
<div class="form-group">
<label for="credentials_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="credentials_username" ng-model="model.Username">
<input type="text" class="form-control" id="credentials_username" ng-model="model.Username" />
</div>
</div>
<!-- !credentials-user -->
@ -58,7 +57,7 @@
<div class="form-group">
<label for="credentials_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="credentials_password" ng-model="model.Password" placeholder="*******">
<input type="password" class="form-control" id="credentials_password" ng-model="model.Password" placeholder="*******" />
</div>
</div>
<!-- !credentials-password -->
@ -73,9 +72,7 @@
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to connect to the registry API with TLS."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="model.TLS"><i></i>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="model.TLS" /><i></i> </label>
</div>
</div>
<!-- !tls-checkbox -->
@ -86,9 +83,7 @@
Skip certificate verification
<portainer-tooltip position="bottom" message="Skip the verification of the server TLS certificate. Not recommended on unsecured networks."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="model.TLSSkipVerify"><i></i>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="model.TLSSkipVerify" /><i></i> </label>
</div>
</div>
<!-- !tls-skip-verify -->
@ -104,7 +99,11 @@
<button class="btn btn-sm btn-primary" ngf-select ng-model="model.TLSCACertFile">Select file</button>
<span style="margin-left: 5px;">
{{ model.TLSCACertFile.name }}
<i class="fa fa-check green-icon" ng-if="model.TLSCACertFile && model.TLSCACertFile === registry.ManagementConfiguration.TLSConfig.TLSCACertFile" aria-hidden="true"></i>
<i
class="fa fa-check green-icon"
ng-if="model.TLSCACertFile && model.TLSCACertFile === registry.ManagementConfiguration.TLSConfig.TLSCACertFile"
aria-hidden="true"
></i>
<i class="fa fa-times red-icon" ng-if="!model.TLSCACertFile" aria-hidden="true"></i>
</span>
</div>
@ -117,7 +116,11 @@
<button class="btn btn-sm btn-primary" ngf-select ng-model="model.TLSCertFile">Select file</button>
<span style="margin-left: 5px;">
{{ model.TLSCertFile.name }}
<i class="fa fa-check green-icon" ng-if="model.TLSCertFile && model.TLSCertFile === registry.ManagementConfiguration.TLSConfig.TLSCertFile" aria-hidden="true"></i>
<i
class="fa fa-check green-icon"
ng-if="model.TLSCertFile && model.TLSCertFile === registry.ManagementConfiguration.TLSConfig.TLSCertFile"
aria-hidden="true"
></i>
<i class="fa fa-times red-icon" ng-if="!model.TLSCertFile" aria-hidden="true"></i>
</span>
</div>
@ -148,7 +151,13 @@
<span ng-hide="state.testInProgress">Test configuration</span>
<span ng-show="state.testInProgress">Test in progress...</span>
</button>
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.updateInProgress || !state.validConfiguration" ng-click="updateConfiguration()" button-spinner="state.updateInProgress">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.updateInProgress || !state.validConfiguration"
ng-click="updateConfiguration()"
button-spinner="state.updateInProgress"
>
<span ng-hide="state.updateInProgress">Save configuration</span>
<span ng-show="state.updateInProgress">Saving configuration...</span>
</button>

View file

@ -6,8 +6,6 @@
{{ $ctrl.resolve.message }}
</p>
</span>
<span>
&nbsp; {{ $ctrl.resolve.progressLabel }} : {{ $ctrl.resolve.context.progression }}% - {{ $ctrl.resolve.context.elapsedTime |number:0 }}s
</span>
<span> &nbsp; {{ $ctrl.resolve.progressLabel }} : {{ $ctrl.resolve.context.progression }}% - {{ $ctrl.resolve.context.elapsedTime | number: 0 }}s </span>
</rd-widget-body>
</rd-widget>
</rd-widget>

View file

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

View file

@ -5,8 +5,7 @@
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="portainer.registries">Registries</a> &gt;
<a ui-sref="portainer.registries.registry.repositories({id: registry.Id})">{{ registry.Name }}</a> &gt;
<a ui-sref="portainer.registries">Registries</a> &gt; <a ui-sref="portainer.registries.registry.repositories({id: registry.Id})">{{ registry.Name }}</a> &gt;
<a ui-sref="portainer.registries.registry.repository()">{{ repository.Name }} </a>
</rd-header-content>
</rd-header>
@ -16,22 +15,20 @@
<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.
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>
<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
&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
<i class="fa fa-check-circle green-icon"></i> Retrieval completed in {{ state.tagsRetrieval.elapsedTime | number: 0 }}s
</span>
</information-panel>
</div>
@ -39,8 +36,7 @@
<div class="row">
<div class="col-sm-8">
<rd-widget>
<rd-widget-header icon="fa-info" title-text="Repository information">
</rd-widget-header>
<rd-widget-header icon="fa-info" title-text="Repository information"> </rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
@ -73,16 +69,19 @@
<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-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
<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 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">
<input type="text" class="form-control" id="tag" ng-model="formValues.Tag" />
</div>
</div>
<div class="form-group">
@ -98,8 +97,13 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.Tag || !formValues.SelectedImage"
ng-click="addTag()" button-spinner="state.actionInProgress">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !formValues.Tag || !formValues.SelectedImage"
ng-click="addTag()"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress">Add tag</span>
<span ng-show="state.actionInProgress">Adding tag...</span>
</button>
@ -113,11 +117,18 @@
<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
title-text="Tags"
title-icon="fa-tags"
dataset="tags"
table-key="registryRepositoryTags"
order-by="Name"
remove-action="removeTags"
retag-action="retagAction"
advanced-features-available="short.Images.length > 0"
pagination-action="paginationAction"
loading="state.loading">
loading="state.loading"
>
</registries-repository-tags-datatable>
</div>
</div>
</div>

View file

@ -1,418 +1,423 @@
import _ from 'lodash-es';
import { RepositoryTagViewModel, RepositoryShortTag } from '../../../models/repositoryTag';
angular.module('portainer.app')
.controller('RegistryRepositoryController', ['$q', '$async', '$scope', '$uibModal', '$interval', '$transition$', '$state', 'RegistryServiceSelector', 'RegistryService', 'ModalService', 'Notifications', 'ImageHelper',
function ($q, $async, $scope, $uibModal, $interval, $transition$, $state, RegistryServiceSelector, RegistryService, ModalService, Notifications, ImageHelper) {
angular.module('portainer.app').controller('RegistryRepositoryController', [
'$q',
'$async',
'$scope',
'$uibModal',
'$interval',
'$transition$',
'$state',
'RegistryServiceSelector',
'RegistryService',
'ModalService',
'Notifications',
'ImageHelper',
function ($q, $async, $scope, $uibModal, $interval, $transition$, $state, RegistryServiceSelector, RegistryService, ModalService, Notifications, ImageHelper) {
$scope.state = {
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: '', // new tag name on add feature
};
$scope.tags = []; // RepositoryTagViewModel (for datatable)
$scope.short = {
Tags: [], // RepositoryShortTag
Images: [], // strings extracted from short.Tags
};
$scope.repository = {
Name: '',
Tags: [], // string list
};
$scope.state = {
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: '' // new tag name on add feature
};
$scope.tags = []; // RepositoryTagViewModel (for datatable)
$scope.short = {
Tags: [], // RepositoryShortTag
Images: [] // strings extracted from short.Tags
};
$scope.repository = {
Name: '',
Tags: [], // string list
};
function toSeconds(time) {
return time / 1000;
}
function toPercent(progress, total) {
return ((progress / total) * 100).toFixed();
}
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,
});
}
function openModal(resolve) {
return $uibModal.open({
component: 'progressionModal',
backdrop: 'static',
keyboard: false,
resolve: resolve
});
}
$scope.paginationAction = function (tags) {
$scope.state.loading = true;
RegistryServiceSelector.getTagsDetails($scope.registry, $scope.repository.Name, tags)
$scope.paginationAction = function (tags) {
$scope.state.loading = true;
RegistryServiceSelector.getTagsDetails($scope.registry, $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});
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) {
})
.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);
}
/**
* RETRIEVAL SECTION
*/
function updateRetrievalClock(startTime) {
$scope.state.tagsRetrieval.elapsedTime = toSeconds(Date.now() - startTime);
}
function createRetrieveAsyncGenerator() {
$scope.state.tagsRetrieval.asyncGenerator =
RegistryServiceSelector.shortTagsWithProgress($scope.registry, $scope.repository.Name, $scope.repository.Tags);
}
function createRetrieveAsyncGenerator() {
$scope.state.tagsRetrieval.asyncGenerator = RegistryServiceSelector.shortTagsWithProgress($scope.registry, $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 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), '');
}
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;
$scope.startStopRetrieval = function () {
if ($scope.state.tagsRetrieval.running) {
$scope.state.tagsRetrieval.asyncGenerator.return();
$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 RegistryServiceSelector.addTag($scope.registry, $scope.repository.Name, $scope.formValues.Tag, 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 () {
return $async(addTagAsync);
};
/**
* !END ADD TAG SECTION
*/
/**
* RETAG SECTION
*/
function updateRetagClock(startTime) {
$scope.state.tagsRetag.elapsedTime = toSeconds(Date.now() - startTime);
}
function createRetagAsyncGenerator(modifiedTags, modifiedDigests, impactedTags) {
$scope.state.tagsRetag.asyncGenerator =
RegistryServiceSelector.retagWithProgress($scope.registry, $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));
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 =
RegistryServiceSelector.deleteTagsWithProgress($scope.registry, $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');
} else {
retrieveTags().then(() => {
createRetrieveAsyncGenerator();
if ($scope.short.Tags.length === 0) {
$state.go('portainer.registries.registry.repositories', {id: $scope.registry.Id}, {reload: true});
resetTagsRetrievalState();
} else {
computeImages();
}
await loadRepositoryDetails();
} catch (err) {
Notifications.error('Failure', err, 'Unable to delete tags');
} finally {
$interval.cancel($scope.state.tagsDelete.clock);
$scope.state.tagsDelete.running = false;
});
}
};
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 RegistryServiceSelector.addTag($scope.registry, $scope.repository.Name, $scope.formValues.Tag, 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 () {
return $async(addTagAsync);
};
/**
* !END ADD TAG SECTION
*/
/**
* RETAG SECTION
*/
function updateRetagClock(startTime) {
$scope.state.tagsRetag.elapsedTime = toSeconds(Date.now() - startTime);
}
function createRetagAsyncGenerator(modifiedTags, modifiedDigests, impactedTags) {
$scope.state.tagsRetag.asyncGenerator = RegistryServiceSelector.retagWithProgress($scope.registry, $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));
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.removeTags = function(selectedItems) {
ModalService.confirmDeletion(
'Are you sure you want to remove the selected tags ?',
(confirmed) => {
if (!confirmed) {
return;
}
return $async(removeTagsAsync, selectedItems);
});
}
/**
* !END REMOVE TAGS SECTION
*/
$scope.retagAction = function () {
return $async(retagActionAsync);
};
/**
* !END RETAG SECTION
*/
/**
* REMOVE REPOSITORY SECTION
*/
async function removeRepositoryAsync() {
try {
const digests = _.uniqBy($scope.short.Tags, 'ImageDigest');
const promises = [];
_.map(digests, (item) => promises.push(RegistryServiceSelector.deleteManifest($scope.registry, $scope.repository.Name, item.ImageDigest)));
await Promise.all(promises);
Notifications.success('Success', 'Repository sucessfully removed');
$state.go('portainer.registries.registry.repositories', {id: $scope.registry.Id}, {reload: true});
} catch (err) {
Notifications.error('Failure', err, 'Unable to delete repository');
}
}
/**
* REMOVE TAGS SECTION
*/
$scope.removeRepository = function () {
ModalService.confirmDeletion(
'This action will only remove the manifests linked to this repository. You need to manually trigger a garbage collector pass on your registry to remove orphan layers and really remove the images content. THIS ACTION CAN NOT BE UNDONE',
function onConfirm(confirmed) {
if (!confirmed) {
return;
}
return $async(removeRepositoryAsync);
function updateDeleteClock(startTime) {
$scope.state.tagsDelete.elapsedTime = toSeconds(Date.now() - startTime);
}
function createDeleteAsyncGenerator(modifiedDigests, impactedTags) {
$scope.state.tagsDelete.asyncGenerator = RegistryServiceSelector.deleteTagsWithProgress($scope.registry, $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);
}
);
};
/**
* !END REMOVE REPOSITORY SECTION
*/
/**
* INIT SECTION
*/
async function loadRepositoryDetails() {
try {
const registry = $scope.registry;
const repository = $scope.repository.Name;
const tags = await RegistryServiceSelector.tags(registry, 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');
}
_.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.registry.Id }, { 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();
}
}
async function initView() {
try {
const registryId = $transition$.params().id;
$scope.repository.Name = $transition$.params().repository;
$scope.state.loading = true;
$scope.removeTags = function (selectedItems) {
ModalService.confirmDeletion('Are you sure you want to remove the selected tags ?', (confirmed) => {
if (!confirmed) {
return;
}
return $async(removeTagsAsync, selectedItems);
});
};
/**
* !END REMOVE TAGS SECTION
*/
$scope.registry = await RegistryService.registry(registryId);
await loadRepositoryDetails();
if ($scope.repository.Tags.length > $scope.state.tagsRetrieval.limit) {
$scope.state.tagsRetrieval.auto = false;
/**
* REMOVE REPOSITORY SECTION
*/
async function removeRepositoryAsync() {
try {
const digests = _.uniqBy($scope.short.Tags, 'ImageDigest');
const promises = [];
_.map(digests, (item) => promises.push(RegistryServiceSelector.deleteManifest($scope.registry, $scope.repository.Name, item.ImageDigest)));
await Promise.all(promises);
Notifications.success('Success', 'Repository sucessfully removed');
$state.go('portainer.registries.registry.repositories', { id: $scope.registry.Id }, { reload: true });
} catch (err) {
Notifications.error('Failure', err, 'Unable to delete repository');
}
}
$scope.removeRepository = function () {
ModalService.confirmDeletion(
'This action will only remove the manifests linked to this repository. You need to manually trigger a garbage collector pass on your registry to remove orphan layers and really remove the images content. THIS ACTION CAN NOT BE UNDONE',
function onConfirm(confirmed) {
if (!confirmed) {
return;
}
createRetrieveAsyncGenerator();
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve repository information');
} finally {
$scope.state.loading = false;
return $async(removeRepositoryAsync);
}
}
);
};
/**
* !END REMOVE REPOSITORY SECTION
*/
$scope.$on('$destroy', () => {
if ($scope.state.tagsRetrieval.asyncGenerator) {
$scope.state.tagsRetrieval.asyncGenerator.return();
/**
* INIT SECTION
*/
async function loadRepositoryDetails() {
try {
const registry = $scope.registry;
const repository = $scope.repository.Name;
const tags = await RegistryServiceSelector.tags(registry, 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');
}
}
async function initView() {
try {
const 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;
}
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);
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();
}
});
this.$onInit = function() {
return $async(initView)
.then(() => {
if ($scope.state.tagsRetrieval.auto) {
$scope.startStopRetrieval();
}
});
};
/**
* !END INIT SECTION
*/
}
]);
};
/**
* !END INIT SECTION
*/
},
]);

View file

@ -5,7 +5,9 @@
</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})" ui-sref-opts="{reload:true}">{{ 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>
@ -16,11 +18,12 @@
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Portainer was not able to use this registry management features. You might need to update the configuration used by Portainer to access this registry.
</p>
<p>Note: Portainer registry management features are only supported with registries exposing the <a href="https://docs.docker.com/registry/spec/api/" target="_blank">v2 registry API</a>.</p>
<p
>Note: Portainer registry management features are only supported with registries exposing the
<a href="https://docs.docker.com/registry/spec/api/" target="_blank">v2 registry API</a>.</p
>
<div style="margin-top: 7px;">
<a ui-sref="portainer.registries.registry.configure({id: registry.Id})">
<i class="fa fa-wrench" aria-hidden="true"></i> Configure this registry
</a>
<a ui-sref="portainer.registries.registry.configure({id: registry.Id})"> <i class="fa fa-wrench" aria-hidden="true"></i> Configure this registry </a>
</div>
</span>
</information-panel>
@ -29,9 +32,14 @@
<div class="row" ng-if="repositories">
<div class="col-sm-12">
<registry-repositories-datatable
title-text="Repositories" title-icon="fa-book"
dataset="repositories" table-key="registryRepositories"
order-by="Name" pagination-action="paginationAction" loading="state.loading">
title-text="Repositories"
title-icon="fa-book"
dataset="repositories"
table-key="registryRepositories"
order-by="Name"
pagination-action="paginationAction"
loading="state.loading"
>
</registry-repositories-datatable>
</div>
</div>

View file

@ -2,64 +2,70 @@ import _ from 'lodash-es';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
angular.module('portainer.extensions.registrymanagement')
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryServiceSelector', 'Notifications', 'Authentication',
function ($transition$, $scope, RegistryService, RegistryServiceSelector, Notifications, Authentication) {
angular.module('portainer.extensions.registrymanagement').controller('RegistryRepositoriesController', [
'$transition$',
'$scope',
'RegistryService',
'RegistryServiceSelector',
'Notifications',
'Authentication',
function ($transition$, $scope, RegistryService, RegistryServiceSelector, Notifications, Authentication) {
$scope.state = {
displayInvalidConfigurationMessage: false,
loading: false,
};
$scope.state = {
displayInvalidConfigurationMessage: false,
loading: false
};
$scope.paginationAction = function (repositories) {
if ($scope.registry.Type === RegistryTypes.GITLAB) {
return;
}
$scope.state.loading = true;
RegistryServiceSelector.getRepositoriesDetails($scope.registry, 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.paginationAction = function (repositories) {
if ($scope.registry.Type === RegistryTypes.GITLAB) {
return;
}
$scope.state.loading = false;
}).catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve repositories details');
});
};
$scope.state.loading = true;
RegistryServiceSelector.getRepositoriesDetails($scope.registry, 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() {
const registryId = $transition$.params().id;
function initView() {
const registryId = $transition$.params().id;
var authenticationEnabled = $scope.applicationState.application.authentication;
if (authenticationEnabled) {
$scope.isAdmin = Authentication.isAdmin();
var authenticationEnabled = $scope.applicationState.application.authentication;
if (authenticationEnabled) {
$scope.isAdmin = Authentication.isAdmin();
}
RegistryService.registry(registryId)
.then(function success(data) {
$scope.registry = data;
RegistryServiceSelector.ping($scope.registry, false)
.then(function success() {
return RegistryServiceSelector.repositories($scope.registry);
})
.then(function success(data) {
$scope.repositories = data;
})
.catch(function error() {
$scope.state.displayInvalidConfigurationMessage = true;
});
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registry details');
});
}
RegistryService.registry(registryId)
.then(function success(data) {
$scope.registry = data;
RegistryServiceSelector.ping($scope.registry, false)
.then(function success() {
return RegistryServiceSelector.repositories($scope.registry);
})
.then(function success(data) {
$scope.repositories = data;
})
.catch(function error() {
$scope.state.displayInvalidConfigurationMessage = true;
});
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registry details');
});
}
initView();
}]);
initView();
},
]);

View file

@ -5,8 +5,7 @@
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="portainer.registries">Registries</a> &gt;
<a ui-sref="portainer.registries.registry.repositories()">{{ ctrl.registry.Name }}</a> &gt;
<a ui-sref="portainer.registries">Registries</a> &gt; <a ui-sref="portainer.registries.registry.repositories()">{{ ctrl.registry.Name }}</a> &gt;
<a ui-sref="portainer.registries.registry.repository()">{{ ctrl.context.repository }} </a> &gt;
{{ ctrl.context.tag }}
</rd-header-content>
@ -20,8 +19,8 @@
<form class="form-horizontal">
<div class="form-group">
<div class="row">
<div class="pull-left" ng-repeat="tag in ctrl.details.RepoTags" style="display:table">
<div class="input-group col-md-1" style="padding:0 15px">
<div class="pull-left" ng-repeat="tag in ctrl.details.RepoTags" style="display: table;">
<div class="input-group col-md-1" style="padding: 0 15px;">
<span class="input-group-addon">{{ tag }}</span>
</div>
</div>
@ -52,11 +51,11 @@
</tr>
<tr>
<td>Created</td>
<td>{{ ctrl.details.Created|getisodate }}</td>
<td>{{ ctrl.details.Created | getisodate }}</td>
</tr>
<tr>
<td>Build</td>
<td>Docker {{ ctrl.details.DockerVersion }} on {{ ctrl.details.Os}}, {{ ctrl.details.Architecture }}</td>
<td>Docker {{ ctrl.details.DockerVersion }} on {{ ctrl.details.Os }}, {{ ctrl.details.Architecture }}</td>
</tr>
<tr ng-if="ctrl.details.Author">
<td>Author</td>
@ -78,11 +77,15 @@
<tbody>
<tr>
<td>CMD</td>
<td><code>{{ ctrl.details.Command|command }}</code></td>
<td
><code>{{ ctrl.details.Command | command }}</code></td
>
</tr>
<tr ng-if="ctrl.details.Entrypoint">
<td>ENTRYPOINT</td>
<td><code>{{ ctrl.details.Entrypoint|command }}</code></td>
<td
><code>{{ ctrl.details.Entrypoint | command }}</code></td
>
</tr>
<tr ng-if="ctrl.details.ExposedPorts.length > 0">
<td>EXPOSE</td>
@ -116,7 +119,6 @@
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="ctrl.tag">
@ -126,7 +128,7 @@
<rd-widget-body classes="no-padding">
<table id="image-layers" class="table">
<thead>
<th style="white-space:nowrap;">
<th style="white-space: nowrap;">
<a ng-click="ctrl.order('Order')">
Order
<span ng-show="ctrl.Sort.Type == 'Order' && !ctrl.Sort.Reverse" class="glyphicon glyphicon-chevron-down"></span>
@ -143,25 +145,25 @@
</thead>
<tbody>
<tr ng-repeat="layer in ctrl.history | orderBy:ctrl.Sort.Type:ctrl.Sort.Reverse">
<td style="white-space:nowrap;">
<td style="white-space: nowrap;">
{{ layer.Order }}
</td>
<td class="expand">
<div ng-if="layer.CreatedBy.length > 130">
<span id="layer-command-{{$index}}-full" style="display: none">
<span id="layer-command-{{ $index }}-full" style="display: none;">
{{ layer.CreatedBy }}
</span>
<span id="layer-command-{{$index}}-short">
{{ layer.CreatedBy | truncate:130 }}
<span id="layer-command-{{ $index }}-short">
{{ layer.CreatedBy | truncate: 130 }}
<span ng-if="layer.CreatedBy.length > 130" style="margin-left: 5px;">
<a id="layer-command-expander{{$index}}" class="btn" ng-click='ctrl.toggleLayerCommand($index)'>
<a id="layer-command-expander{{ $index }}" class="btn" ng-click="ctrl.toggleLayerCommand($index)">
<i class="fa fa-plus-circle" aria-hidden="true"></i>
</a>
</span>
</span>
</div>
<div ng-if="layer.CreatedBy.length <= 130">
<span id="layer-command-{{$index}}-full">
<span id="layer-command-{{ $index }}-full">
{{ layer.CreatedBy }}
</span>
</div>
@ -172,4 +174,4 @@
</rd-widget-body>
</rd-widget>
</div>
</div>
</div>

View file

@ -4,7 +4,6 @@ import { RegistryImageLayerViewModel } from 'Extensions/registry-management/mode
import { RegistryImageDetailsViewModel } from 'Extensions/registry-management/models/registryImageDetails';
class RegistryRepositoryTagController {
/* @ngInject */
constructor($transition$, $async, Notifications, RegistryService, RegistryServiceSelector, imagelayercommandFilter) {
this.$transition$ = $transition$;
@ -19,13 +18,13 @@ class RegistryRepositoryTagController {
}
toggleLayerCommand(layerId) {
$('#layer-command-expander'+layerId+' span').toggleClass('glyphicon-plus-sign glyphicon-minus-sign');
$('#layer-command-'+layerId+'-short').toggle();
$('#layer-command-'+layerId+'-full').toggle();
}
$('#layer-command-expander' + layerId + ' span').toggleClass('glyphicon-plus-sign glyphicon-minus-sign');
$('#layer-command-' + layerId + '-short').toggle();
$('#layer-command-' + layerId + '-full').toggle();
}
order(sortType) {
this.Sort.Reverse = (this.Sort.Type === sortType) ? !this.Sort.Reverse : false;
this.Sort.Reverse = this.Sort.Type === sortType ? !this.Sort.Reverse : false;
this.Sort.Type = sortType;
}
@ -35,17 +34,17 @@ class RegistryRepositoryTagController {
this.context.tag = this.$transition$.params().tag;
this.Sort = {
Type: 'Order',
Reverse: false
}
Reverse: false,
};
try {
this.registry = await this.RegistryService.registry(this.context.registryId);
this.tag = await this.RegistryServiceSelector.tag(this.registry, this.context.repository, this.context.tag);
const length = this.tag.History.length;
this.history = _.map(this.tag.History, (layer, idx) => new RegistryImageLayerViewModel(length - idx, layer));
_.forEach(this.history, (item) => item.CreatedBy = this.imagelayercommandFilter(item.CreatedBy))
_.forEach(this.history, (item) => (item.CreatedBy = this.imagelayercommandFilter(item.CreatedBy)));
this.details = new RegistryImageDetailsViewModel(this.tag.History[0]);
} catch (error) {
this.Notifications.error('Failure', error, 'Unable to retrieve tag')
this.Notifications.error('Failure', error, 'Unable to retrieve tag');
}
}