Compare commits
10 Commits
fix/EE-186
...
fix/releas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8876c2d134 | ||
|
|
685552a661 | ||
|
|
1b0e58a4e8 | ||
|
|
151dfe7e65 | ||
|
|
ed89587cb9 | ||
|
|
dad762de9f | ||
|
|
661931d8b0 | ||
|
|
84e57cebc9 | ||
|
|
fd9427cd0b | ||
|
|
e60dbba93b |
@@ -74,7 +74,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.9.2
|
||||
// @version 2.9.1
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
@@ -54,8 +54,11 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !govalidator.IsURL(*payload.TemplatesURL) {
|
||||
return errors.New("Invalid external templates URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" && !govalidator.IsURL(*payload.HelmRepositoryURL) {
|
||||
return errors.New("Invalid Helm repository URL. Must correspond to a valid URL format")
|
||||
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" {
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid Helm repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
}
|
||||
if payload.UserSessionTimeout != nil {
|
||||
_, err := time.ParseDuration(*payload.UserSessionTimeout)
|
||||
@@ -111,16 +114,7 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
if payload.HelmRepositoryURL != nil {
|
||||
newHelmRepo := strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
|
||||
|
||||
if newHelmRepo != settings.HelmRepositoryURL && newHelmRepo != portainer.DefaultHelmRepositoryURL {
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Helm repository URL. Must correspond to a valid URL format", err}
|
||||
}
|
||||
}
|
||||
|
||||
settings.HelmRepositoryURL = newHelmRepo
|
||||
settings.HelmRepositoryURL = strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
|
||||
}
|
||||
|
||||
if payload.BlackListedLabels != nil {
|
||||
|
||||
@@ -46,19 +46,5 @@ func (transport *baseTransport) proxyNamespaceDeleteOperation(request *http.Requ
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stacks, err := transport.dataStore.Stack().Stacks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, s := range stacks {
|
||||
if s.Namespace == namespace && s.EndpointID == transport.endpoint.ID {
|
||||
if err := transport.dataStore.Stack().DeleteStack(s.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transport.executeKubernetesRequest(request)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
|
||||
return []rbacv1.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"list", "get"},
|
||||
Verbs: []string{"list"},
|
||||
Resources: []string{"namespaces", "nodes"},
|
||||
APIGroups: []string{""},
|
||||
},
|
||||
@@ -18,11 +18,6 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
|
||||
Resources: []string{"storageclasses"},
|
||||
APIGroups: []string{"storage.k8s.io"},
|
||||
},
|
||||
{
|
||||
Verbs: []string{"list", "get"},
|
||||
Resources: []string{"namespaces", "pods", "nodes"},
|
||||
APIGroups: []string{"metrics.k8s.io"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1470,7 +1470,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.9.2"
|
||||
APIVersion = "2.9.1"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 32
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
|
||||
@@ -62,6 +62,7 @@ html {
|
||||
--grey-58: #ebf4f8;
|
||||
--grey-59: #e6e6e6;
|
||||
--grey-60: #cacaca;
|
||||
--grey-61: rgb(231, 231, 231);
|
||||
|
||||
--blue-1: #219;
|
||||
--blue-2: #337ab7;
|
||||
@@ -158,6 +159,8 @@ html {
|
||||
--bg-small-select-color: var(--white-color);
|
||||
--bg-app-datatable-thead: var(--grey-23);
|
||||
--bg-app-datatable-tbody: var(--grey-24);
|
||||
--bg-stepper-item-active: var(--white-color);
|
||||
--bg-stepper-item-counter: var(--grey-61);
|
||||
|
||||
--text-main-color: var(--grey-7);
|
||||
--text-body-color: var(--grey-6);
|
||||
@@ -328,6 +331,8 @@ html {
|
||||
--bg-small-select-color: var(--grey-2);
|
||||
--bg-app-datatable-thead: var(--grey-1);
|
||||
--bg-app-datatable-tbody: var(--grey-1);
|
||||
--bg-stepper-item-active: var(--grey-1);
|
||||
--bg-stepper-item-counter: var(--grey-7);
|
||||
|
||||
--text-main-color: var(--white-color);
|
||||
--text-body-color: var(--white-color);
|
||||
@@ -497,6 +502,8 @@ html {
|
||||
--bg-small-select-color: var(--black-color);
|
||||
--bg-app-datatable-thead: var(--black-color);
|
||||
--bg-app-datatable-tbody: var(--black-color);
|
||||
--bg-stepper-item-active: var(--black-color);
|
||||
--bg-stepper-item-counter: var(--grey-3);
|
||||
|
||||
--text-main-color: var(--white-color);
|
||||
--text-body-color: var(--white-color);
|
||||
|
||||
@@ -31,3 +31,4 @@ angular
|
||||
|
||||
export const PORTAINER_FADEOUT = 1500;
|
||||
export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||
export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||
|
||||
@@ -69,19 +69,13 @@ class porImageRegistryController {
|
||||
async reloadRegistries() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
let showDefaultRegistry = false;
|
||||
this.registries = await this.EndpointService.registries(this.endpoint.Id, this.namespace);
|
||||
|
||||
// hide default(anonymous) dockerhub registry if user has an authenticated one
|
||||
if (!this.registries.some((registry) => registry.Type === RegistryTypes.DOCKERHUB)) {
|
||||
showDefaultRegistry = true;
|
||||
this.registries.push(this.defaultRegistry);
|
||||
}
|
||||
const registries = await this.EndpointService.registries(this.endpoint.Id, this.namespace);
|
||||
this.registries = _.concat(this.defaultRegistry, registries);
|
||||
|
||||
const id = this.model.Registry.Id;
|
||||
const registry = _.find(this.registries, { Id: id });
|
||||
if (!registry) {
|
||||
this.model.Registry = showDefaultRegistry ? this.defaultRegistry : this.registries[0];
|
||||
this.model.Registry = this.defaultRegistry;
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve registries');
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</label>
|
||||
<div ng-class="$ctrl.inputClass">
|
||||
<select
|
||||
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Id"
|
||||
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Name"
|
||||
ng-model="$ctrl.model.Registry"
|
||||
id="image_registry"
|
||||
class="form-control"
|
||||
|
||||
@@ -17,8 +17,6 @@ angular.module('portainer.docker').controller('ImageController', [
|
||||
'FileSaver',
|
||||
'Blob',
|
||||
'endpoint',
|
||||
'EndpointService',
|
||||
'RegistryModalService',
|
||||
function (
|
||||
$async,
|
||||
$q,
|
||||
@@ -34,9 +32,7 @@ angular.module('portainer.docker').controller('ImageController', [
|
||||
ModalService,
|
||||
FileSaver,
|
||||
Blob,
|
||||
endpoint,
|
||||
EndpointService,
|
||||
RegistryModalService
|
||||
endpoint
|
||||
) {
|
||||
$scope.endpoint = endpoint;
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
@@ -88,13 +84,11 @@ angular.module('portainer.docker').controller('ImageController', [
|
||||
|
||||
async function pushTag(repository) {
|
||||
return $async(async () => {
|
||||
$('#uploadResourceHint').show();
|
||||
try {
|
||||
const registryModel = await RegistryModalService.registryModal(repository, $scope.registries);
|
||||
if (registryModel) {
|
||||
$('#uploadResourceHint').show();
|
||||
await ImageService.pushImage(registryModel);
|
||||
Notifications.success('Image successfully pushed', repository);
|
||||
}
|
||||
const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
|
||||
await ImageService.pushImage(registryModel);
|
||||
Notifications.success('Image successfully pushed', repository);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to push image to repository');
|
||||
} finally {
|
||||
@@ -106,13 +100,11 @@ angular.module('portainer.docker').controller('ImageController', [
|
||||
$scope.pullTag = pullTag;
|
||||
async function pullTag(repository) {
|
||||
return $async(async () => {
|
||||
$('#downloadResourceHint').show();
|
||||
try {
|
||||
const registryModel = await RegistryModalService.registryModal(repository, $scope.registries);
|
||||
if (registryModel) {
|
||||
$('#downloadResourceHint').show();
|
||||
await ImageService.pullImage(registryModel);
|
||||
Notifications.success('Image successfully pulled', repository);
|
||||
}
|
||||
const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
|
||||
await ImageService.pullImage(registryModel);
|
||||
Notifications.success('Image successfully pulled', repository);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to pull image from repository');
|
||||
} finally {
|
||||
@@ -179,15 +171,8 @@ angular.module('portainer.docker').controller('ImageController', [
|
||||
});
|
||||
};
|
||||
|
||||
async function initView() {
|
||||
function initView() {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName);
|
||||
|
||||
try {
|
||||
$scope.registries = await RegistryService.loadRegistriesForDropdown(endpoint.Id);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load registries');
|
||||
}
|
||||
|
||||
$q.all({
|
||||
image: ImageService.image($transition$.params().id),
|
||||
history: ImageService.history($transition$.params().id),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
export default class HelmTemplatesController {
|
||||
/* @ngInject */
|
||||
@@ -141,8 +142,8 @@ export default class HelmTemplatesController {
|
||||
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
|
||||
const nonSystemNamespaces = resourcePools.filter((resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
this.state.resourcePools = nonSystemNamespaces;
|
||||
this.state.resourcePool = nonSystemNamespaces[0];
|
||||
this.state.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
||||
this.state.resourcePool = this.state.resourcePools[0];
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve initial helm data.');
|
||||
} finally {
|
||||
|
||||
@@ -1771,7 +1771,7 @@
|
||||
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.imageValidityIsValid()"
|
||||
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity"
|
||||
ng-click="ctrl.deployApplication()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
data-cy="k8sAppCreate-deployButton"
|
||||
|
||||
@@ -2,7 +2,6 @@ import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
import { RegistryTypes } from '@/portainer/models/registryTypes';
|
||||
|
||||
import {
|
||||
KubernetesApplicationDataAccessPolicies,
|
||||
@@ -194,10 +193,6 @@ class KubernetesCreateApplicationController {
|
||||
this.state.pullImageValidity = validity;
|
||||
}
|
||||
|
||||
imageValidityIsValid() {
|
||||
return this.state.pullImageValidity || this.formValues.ImageModel.Registry.Type !== RegistryTypes.DOCKERHUB;
|
||||
}
|
||||
|
||||
onChangeName() {
|
||||
const existingApplication = _.find(this.applications, { Name: this.formValues.Name });
|
||||
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);
|
||||
@@ -1079,7 +1074,10 @@ class KubernetesCreateApplicationController {
|
||||
]);
|
||||
this.nodesLimits = nodesLimits;
|
||||
|
||||
this.resourcePools = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
const nonSystemNamespaces = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
|
||||
this.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
||||
|
||||
this.formValues.ResourcePool = this.resourcePools[0];
|
||||
if (!this.formValues.ResourcePool) {
|
||||
return;
|
||||
|
||||
@@ -5,7 +5,27 @@
|
||||
Title
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.formValues.Title" id="template_title" name="template_title" placeholder="e.g. mytemplate" auto-focus required />
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.Title"
|
||||
ng-pattern="$ctrl.nameRegex"
|
||||
id="template_title"
|
||||
name="template_title"
|
||||
placeholder="e.g. mytemplate"
|
||||
auto-focus
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="commonCustomTemplateForm.template_title.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="commonCustomTemplateForm.template_title.$error">
|
||||
<p ng-message="pattern">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<span>This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="commonCustomTemplateForm.template_title.$invalid">
|
||||
|
||||
@@ -7,5 +7,6 @@ angular.module('portainer.app').component('customTemplateCommonFields', {
|
||||
formValues: '=',
|
||||
showPlatformField: '<',
|
||||
showTypeField: '<',
|
||||
nameRegex: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ angular.module('portainer.app').component('stackFromTemplateForm', {
|
||||
state: '=',
|
||||
createTemplate: '<',
|
||||
unselectTemplate: '<',
|
||||
nameRegex: '<',
|
||||
},
|
||||
transclude: {
|
||||
advanced: '?advancedForm',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<rd-widget>
|
||||
<rd-widget-custom-header icon="$ctrl.template.Logo" title-text="$ctrl.template.Title"></rd-widget-custom-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<form class="form-horizontal">
|
||||
<form class="form-horizontal" name="stackTemplateForm">
|
||||
<!-- description -->
|
||||
<div ng-if="$ctrl.template.Note">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
@@ -20,9 +20,19 @@
|
||||
</div>
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
||||
<label for="template_name" class="col-sm-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" name="container_name" class="form-control" ng-model="$ctrl.formValues.name" placeholder="e.g. myStack" required />
|
||||
<input type="text" name="template_name" class="form-control" ng-model="$ctrl.formValues.name" ng-pattern="$ctrl.nameRegex" placeholder="e.g. myStack" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="stackTemplateForm.template_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="stackTemplateForm.template_name.$error">
|
||||
<p ng-message="pattern">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<span>This field must consist of lower case alphanumeric characters, '_' or '-' (e.g. 'my-name', or 'abc-123').</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
|
||||
@@ -22,8 +22,6 @@ angular.module('portainer.app').factory('RegistryService', [
|
||||
createRegistry,
|
||||
createGitlabRegistries,
|
||||
retrievePorRegistryModelFromRepository,
|
||||
retrievePorRegistryModelFromRepositoryWithRegistries,
|
||||
loadRegistriesForDropdown,
|
||||
};
|
||||
|
||||
function registries() {
|
||||
@@ -109,45 +107,17 @@ angular.module('portainer.app').factory('RegistryService', [
|
||||
return url;
|
||||
}
|
||||
|
||||
// findBestMatchRegistry finds out the best match registry for repository
|
||||
// matching precedence:
|
||||
// 1. registryId matched
|
||||
// 2. both domain name and username matched (for dockerhub only)
|
||||
// 3. only URL matched
|
||||
// 4. pick up the first dockerhub registry
|
||||
function findBestMatchRegistry(repository, registries, registryId) {
|
||||
let match2, match3, match4;
|
||||
|
||||
for (const registry of registries) {
|
||||
if (registry.Id == registryId) {
|
||||
return registry;
|
||||
}
|
||||
|
||||
if (registry.Type === RegistryTypes.DOCKERHUB) {
|
||||
// try to match repository examples:
|
||||
// <USERNAME>/nginx:latest
|
||||
// docker.io/<USERNAME>/nginx:latest
|
||||
if (repository.startsWith(registry.Username + '/') || repository.startsWith(getURL(registry) + '/' + registry.Username + '/')) {
|
||||
match2 = registry;
|
||||
}
|
||||
|
||||
// try to match repository examples:
|
||||
// portainer/portainer-ee:latest
|
||||
// <NON-USERNAME>/portainer-ee:latest
|
||||
match4 = match4 || registry;
|
||||
}
|
||||
|
||||
if (_.includes(repository, getURL(registry))) {
|
||||
match3 = registry;
|
||||
}
|
||||
}
|
||||
|
||||
return match2 || match3 || match4;
|
||||
}
|
||||
|
||||
function retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, registryId) {
|
||||
const model = new PorImageRegistryModel();
|
||||
const registry = findBestMatchRegistry(repository, registries, registryId);
|
||||
const registry = registries.find((reg) => {
|
||||
if (registryId) {
|
||||
return reg.Id === registryId;
|
||||
}
|
||||
if (reg.Type === RegistryTypes.DOCKERHUB) {
|
||||
return _.includes(repository, reg.Username);
|
||||
}
|
||||
return _.includes(repository, getURL(reg));
|
||||
});
|
||||
if (registry) {
|
||||
const url = getURL(registry);
|
||||
let lastIndex = repository.lastIndexOf(url);
|
||||
@@ -178,22 +148,5 @@ angular.module('portainer.app').factory('RegistryService', [
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadRegistriesForDropdown(endpointId, namespace) {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const registries = await EndpointService.registries(endpointId, namespace);
|
||||
|
||||
// hide default(anonymous) dockerhub registry if user has an authenticated one
|
||||
if (!registries.some((registry) => registry.Type === RegistryTypes.DOCKERHUB)) {
|
||||
registries.push(new DockerHubViewModel());
|
||||
}
|
||||
|
||||
return registries;
|
||||
} catch (err) {
|
||||
throw { msg: 'Unable to retrieve the registries', err: err };
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -308,17 +308,6 @@ angular.module('portainer.app').factory('ModalService', [
|
||||
);
|
||||
};
|
||||
|
||||
service.selectRegistry = function (options) {
|
||||
var box = bootbox.prompt({
|
||||
title: 'Which registry do you want to use?',
|
||||
inputType: 'select',
|
||||
value: options.defaultValue,
|
||||
inputOptions: options.options,
|
||||
callback: options.callback,
|
||||
});
|
||||
applyBoxCSS(box);
|
||||
};
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
angular.module('portainer.app').factory('RegistryModalService', ModalServiceFactory);
|
||||
|
||||
function ModalServiceFactory($q, ModalService, RegistryService) {
|
||||
const service = {};
|
||||
|
||||
function registries2Options(registries) {
|
||||
return registries.map((r) => ({
|
||||
text: r.Name,
|
||||
value: String(r.Id),
|
||||
}));
|
||||
}
|
||||
|
||||
service.registryModal = async function (repository, registries) {
|
||||
const deferred = $q.defer();
|
||||
|
||||
const options = registries2Options(registries);
|
||||
const registryModel = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries);
|
||||
const defaultValue = String(_.get(registryModel, 'Registry.Id', '0'));
|
||||
|
||||
ModalService.selectRegistry({
|
||||
options,
|
||||
defaultValue,
|
||||
callback: (registryId) => {
|
||||
if (registryId) {
|
||||
const registryModel = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, registryId);
|
||||
deferred.resolve(registryModel);
|
||||
} else {
|
||||
deferred.resolve(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
@@ -8,7 +8,12 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="customTemplateForm">
|
||||
<custom-template-common-fields form-values="$ctrl.formValues" show-platform-field="true" show-type-field="true"></custom-template-common-fields>
|
||||
<custom-template-common-fields
|
||||
form-values="$ctrl.formValues"
|
||||
show-platform-field="true"
|
||||
show-type-field="true"
|
||||
name-regex="$ctrl.state.templateNameRegex"
|
||||
></custom-template-common-fields>
|
||||
|
||||
<!-- build-method -->
|
||||
<div ng-if="!$ctrl.state.fromStack">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
||||
|
||||
class CreateCustomTemplateViewController {
|
||||
/* @ngInject */
|
||||
@@ -43,7 +44,9 @@ class CreateCustomTemplateViewController {
|
||||
fromStack: false,
|
||||
loading: true,
|
||||
isEditorDirty: false,
|
||||
templateNameRegex: TEMPLATE_NAME_VALIDATION_REGEX,
|
||||
};
|
||||
|
||||
this.templates = [];
|
||||
|
||||
this.createCustomTemplate = this.createCustomTemplate.bind(this);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
ng-if="$ctrl.state.selectedTemplate"
|
||||
template="$ctrl.state.selectedTemplate"
|
||||
form-values="$ctrl.formValues"
|
||||
name-regex="$ctrl.state.templateNameRegex"
|
||||
state="$ctrl.state"
|
||||
create-template="$ctrl.createStack"
|
||||
unselect-template="$ctrl.unselectTemplate"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'lodash-es';
|
||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
||||
|
||||
class CustomTemplatesViewController {
|
||||
/* @ngInject */
|
||||
@@ -44,6 +45,7 @@ class CustomTemplatesViewController {
|
||||
actionInProgress: false,
|
||||
isEditorVisible: false,
|
||||
deployable: false,
|
||||
templateNameRegex: TEMPLATE_NAME_VALIDATION_REGEX,
|
||||
};
|
||||
|
||||
this.currentUser = {
|
||||
|
||||
@@ -167,7 +167,15 @@
|
||||
<!-- select-file-input -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button class="btn btn-sm btn-primary" ngf-select accept=".tar.gz,.encrypted" ng-model="formValues.BackupFile" auto-focus>Select file</button>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ngf-select
|
||||
accept=".gz,.encrypted"
|
||||
ngf-accept="'application/x-tar,application/x-gzip'"
|
||||
ng-model="formValues.BackupFile"
|
||||
auto-focus
|
||||
>Select file</button
|
||||
>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.BackupFile.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.BackupFile" aria-hidden="true"></i>
|
||||
|
||||
@@ -30,17 +30,17 @@
|
||||
.stepper-item::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
border-bottom: 5px solid rgb(231, 231, 231);
|
||||
width: 100%;
|
||||
top: 20px;
|
||||
left: -100%;
|
||||
z-index: 2;
|
||||
border-bottom: 5px solid var(--bg-stepper-item-counter);
|
||||
}
|
||||
|
||||
.stepper-item::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
border-bottom: 5px solid rgb(231, 231, 231);
|
||||
border-bottom: 5px solid var(--bg-stepper-item-counter);
|
||||
width: 100%;
|
||||
top: 20px;
|
||||
left: 0;
|
||||
@@ -56,13 +56,13 @@
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgb(231, 231, 231);
|
||||
background: var(--bg-stepper-item-counter);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.stepper-item.active {
|
||||
font-weight: bold;
|
||||
background: #fff;
|
||||
background: var(--bg-stepper-item-active);
|
||||
content: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Portainer.io",
|
||||
"name": "portainer",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "2.9.2",
|
||||
"version": "2.9.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
@@ -176,4 +176,4 @@
|
||||
"*.js": "eslint --cache --fix",
|
||||
"*.{js,css,md,html}": "prettier --write"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user