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:
parent
6663073be1
commit
cf5056d9c0
714 changed files with 31228 additions and 28305 deletions
|
@ -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']);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -2,7 +2,7 @@ angular.module('portainer.extensions.oauth').component('oauthProvidersSelector',
|
|||
templateUrl: './oauth-providers-selector.html',
|
||||
bindings: {
|
||||
onSelect: '<',
|
||||
provider: '='
|
||||
provider: '=',
|
||||
},
|
||||
controller: 'OAuthProviderSelectorController'
|
||||
controller: 'OAuthProviderSelectorController',
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ angular.module('portainer.extensions.oauth').component('oauthSettings', {
|
|||
templateUrl: './oauth-settings.html',
|
||||
bindings: {
|
||||
settings: '=',
|
||||
teams: '<'
|
||||
teams: '<',
|
||||
},
|
||||
controller: 'OAuthSettingsController'
|
||||
controller: 'OAuthSettingsController',
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
}]);
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
angular.module('portainer.app').component('accessViewer', {
|
||||
templateUrl: './accessViewer.html',
|
||||
controller: 'AccessViewerController',
|
||||
controllerAs: 'ctrl'
|
||||
controllerAs: 'ctrl',
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -6,6 +6,6 @@ angular.module('portainer.app').component('accessViewerDatatable', {
|
|||
titleIcon: '@',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
dataset: '<'
|
||||
}
|
||||
dataset: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -8,6 +8,6 @@ angular.module('portainer.extensions.rbac').component('rolesDatatable', {
|
|||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
rbacEnabled: '<'
|
||||
}
|
||||
rbacEnabled: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}]);
|
||||
},
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}]);
|
||||
},
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import angular from 'angular';
|
||||
|
||||
class RolesController {
|
||||
|
||||
/* @ngInject */
|
||||
constructor(Notifications, RoleService, ExtensionService) {
|
||||
this.Notifications = Notifications;
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -9,6 +9,6 @@ angular.module('portainer.extensions.registrymanagement').component('registryRep
|
|||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
paginationAction: '<',
|
||||
loading: '<'
|
||||
}
|
||||
loading: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -12,6 +12,6 @@ angular.module('portainer.extensions.registrymanagement').component('registriesR
|
|||
retagAction: '<',
|
||||
advancedFeaturesAvailable: '<',
|
||||
paginationAction: '<',
|
||||
loading: '<'
|
||||
}
|
||||
loading: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
}]);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -5,4 +5,4 @@ export function RegistryImageLayerViewModel(order, data) {
|
|||
this.Id = data.id;
|
||||
this.Created = data.created;
|
||||
this.CreatedBy = _.join(data.container_config.Cmd, ' ');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,4 +12,4 @@ export function RegistryRepositoryViewModel(item) {
|
|||
export function RegistryRepositoryGitlabViewModel(data) {
|
||||
this.Name = data.path;
|
||||
this.TagsCount = data.tags.length;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}]);
|
||||
]);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -7,4 +7,4 @@ export default function gitlabResponseGetLink(data, headers) {
|
|||
response = data;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,4 +10,4 @@ export default function linkGetResponse(data, headers) {
|
|||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
{{ $ctrl.resolve.message }}
|
||||
</p>
|
||||
</span>
|
||||
<span>
|
||||
{{ $ctrl.resolve.progressLabel }} : {{ $ctrl.resolve.context.progression }}% - {{ $ctrl.resolve.context.elapsedTime |number:0 }}s
|
||||
</span>
|
||||
<span> {{ $ctrl.resolve.progressLabel }} : {{ $ctrl.resolve.context.progression }}% - {{ $ctrl.resolve.context.elapsedTime | number: 0 }}s </span>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</rd-widget>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.extensions.registrymanagement').component('progressionModal', {
|
||||
templateUrl: './progressionModal.html',
|
||||
bindings: {
|
||||
resolve: '<'
|
||||
}
|
||||
resolve: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.registries">Registries</a> >
|
||||
<a ui-sref="portainer.registries.registry.repositories({id: registry.Id})">{{ registry.Name }}</a> >
|
||||
<a ui-sref="portainer.registries">Registries</a> > <a ui-sref="portainer.registries.registry.repositories({id: registry.Id})">{{ registry.Name }}</a> >
|
||||
<a ui-sref="portainer.registries.registry.repository()">{{ repository.Name }} </a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
@ -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'">
|
||||
Retrieval progress : {{ state.tagsRetrieval.progression }}% - {{ state.tagsRetrieval.elapsedTime | number:0 }}s
|
||||
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>
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.registries">Registries</a> > <a ng-if="isAdmin" ui-sref="portainer.registries.registry({id: registry.Id})" ui-sref-opts="{reload:true}">{{ registry.Name }}</a><span ng-if="!isAdmin">{{ registry.Name}}</span> > Repositories
|
||||
<a ui-sref="portainer.registries">Registries</a> >
|
||||
<a ng-if="isAdmin" ui-sref="portainer.registries.registry({id: registry.Id})" ui-sref-opts="{reload:true}">{{ registry.Name }}</a
|
||||
><span ng-if="!isAdmin">{{ registry.Name }}</span> > Repositories
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.registries">Registries</a> >
|
||||
<a ui-sref="portainer.registries.registry.repositories()">{{ ctrl.registry.Name }}</a> >
|
||||
<a ui-sref="portainer.registries">Registries</a> > <a ui-sref="portainer.registries.registry.repositories()">{{ ctrl.registry.Name }}</a> >
|
||||
<a ui-sref="portainer.registries.registry.repository()">{{ ctrl.context.repository }} </a> >
|
||||
{{ 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>
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue