+
@@ -10,49 +10,20 @@
There are no environment variables for this service.
-
-
+
+
+
diff --git a/app/docker/views/services/edit/serviceController.js b/app/docker/views/services/edit/serviceController.js
index cb41d94d8..f1aef75a4 100644
--- a/app/docker/views/services/edit/serviceController.js
+++ b/app/docker/views/services/edit/serviceController.js
@@ -18,6 +18,9 @@ require('./includes/tasks.html');
require('./includes/updateconfig.html');
import _ from 'lodash-es';
+
+import * as envVarsUtils from '@/portainer/helpers/env-vars';
+
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
angular.module('portainer.docker').controller('ServiceController', [
@@ -114,21 +117,25 @@ angular.module('portainer.docker').controller('ServiceController', [
};
$scope.addEnvironmentVariable = function addEnvironmentVariable(service) {
- service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' });
+ service.EnvironmentVariables.push({ name: '', value: '' });
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
};
- $scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, item) {
- const index = service.EnvironmentVariables.indexOf(item);
- const removedElement = service.EnvironmentVariables.splice(index, 1);
- if (removedElement !== null) {
- updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
- }
- };
- $scope.updateEnvironmentVariable = function updateEnvironmentVariable(service, variable) {
- if (variable.value !== variable.originalValue || variable.key !== variable.originalKey) {
- updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
- }
- };
+
+ $scope.onChangeEnvVars = onChangeEnvVars;
+
+ function onChangeEnvVars(env) {
+ const service = $scope.service;
+
+ const orgEnv = service.EnvironmentVariables;
+ service.EnvironmentVariables = env.map((v) => {
+ const orgVar = orgEnv.find(({ name }) => v.name === name);
+ const added = orgVar && orgVar.added;
+ return { ...v, added };
+ });
+
+ updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
+ }
+
$scope.addConfig = function addConfig(service, config) {
if (
config &&
@@ -395,7 +402,7 @@ angular.module('portainer.docker').controller('ServiceController', [
var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.Name;
config.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceLabels);
- config.TaskTemplate.ContainerSpec.Env = ServiceHelper.translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
+ config.TaskTemplate.ContainerSpec.Env = envVarsUtils.convertToArrayOfStrings(service.EnvironmentVariables);
config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceContainerLabels);
if ($scope.hasChanges(service, ['Image'])) {
@@ -625,7 +632,10 @@ angular.module('portainer.docker').controller('ServiceController', [
function translateServiceArrays(service) {
service.ServiceSecrets = service.Secrets ? service.Secrets.map(SecretHelper.flattenSecret) : [];
service.ServiceConfigs = service.Configs ? service.Configs.map(ConfigHelper.flattenConfig) : [];
- service.EnvironmentVariables = ServiceHelper.translateEnvironmentVariables(service.Env);
+ service.EnvironmentVariables = envVarsUtils
+ .parseArrayOfStrings(service.Env)
+ .map((v) => ({ ...v, added: true }))
+ .sort((v1, v2) => (v1.name > v2.name ? 1 : -1));
service.LogDriverOpts = ServiceHelper.translateLogDriverOptsToKeyValue(service.LogDriverOpts);
service.ServiceLabels = LabelHelper.fromLabelHashToKeyValue(service.Labels);
service.ServiceContainerLabels = LabelHelper.fromLabelHashToKeyValue(service.ContainerLabels);
diff --git a/app/portainer/components/environment-variables-panel/environment-variables-panel.controller.js b/app/portainer/components/environment-variables-panel/environment-variables-panel.controller.js
new file mode 100644
index 000000000..9f842e40e
--- /dev/null
+++ b/app/portainer/components/environment-variables-panel/environment-variables-panel.controller.js
@@ -0,0 +1,34 @@
+import { parseDotEnvFile, convertToArrayOfStrings } from '@/portainer/helpers/env-vars';
+
+export default class EnvironmentVariablesPanelController {
+ /* @ngInject */
+ constructor() {
+ this.mode = 'simple';
+ this.editorText = '';
+
+ this.switchEnvMode = this.switchEnvMode.bind(this);
+ this.editorUpdate = this.editorUpdate.bind(this);
+ this.handleSimpleChange = this.handleSimpleChange.bind(this);
+ }
+
+ switchEnvMode() {
+ if (this.mode === 'simple') {
+ const editorText = convertToArrayOfStrings(this.ngModel).join('\n');
+
+ this.editorText = editorText;
+
+ this.mode = 'advanced';
+ } else {
+ this.mode = 'simple';
+ }
+ }
+
+ handleSimpleChange(value) {
+ this.onChange(value);
+ }
+
+ editorUpdate(cm) {
+ this.editorText = cm.getValue();
+ this.onChange(parseDotEnvFile(this.editorText));
+ }
+}
diff --git a/app/portainer/components/environment-variables-panel/environment-variables-panel.css b/app/portainer/components/environment-variables-panel/environment-variables-panel.css
new file mode 100644
index 000000000..84b30444e
--- /dev/null
+++ b/app/portainer/components/environment-variables-panel/environment-variables-panel.css
@@ -0,0 +1,11 @@
+.environment-variables-panel {
+ margin-top: 15px;
+}
+
+.environment-variables-panel--explanation {
+ margin-bottom: 5px;
+}
+
+.environment-variables-panel--advanced > * + * {
+ margin-top: 5px;
+}
diff --git a/app/portainer/components/environment-variables-panel/environment-variables-panel.html b/app/portainer/components/environment-variables-panel/environment-variables-panel.html
new file mode 100644
index 000000000..c84f37ff2
--- /dev/null
+++ b/app/portainer/components/environment-variables-panel/environment-variables-panel.html
@@ -0,0 +1,30 @@
+
+
+
diff --git a/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.controller.js b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.controller.js
new file mode 100644
index 000000000..2138e4f57
--- /dev/null
+++ b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.controller.js
@@ -0,0 +1,41 @@
+import { KEY_REGEX, VALUE_REGEX } from '@/portainer/helpers/env-vars';
+
+class EnvironmentVariablesSimpleModeItemController {
+ /* @ngInject */
+ constructor() {
+ this.KEY_REGEX = KEY_REGEX;
+ this.VALUE_REGEX = VALUE_REGEX;
+ }
+
+ onChangeName(name) {
+ const fieldIsInvalid = typeof name === 'undefined';
+ if (fieldIsInvalid) {
+ return;
+ }
+
+ this.onChange(this.index, { ...this.variable, name });
+ }
+
+ onChangeValue(value) {
+ const fieldIsInvalid = typeof value === 'undefined';
+ if (fieldIsInvalid) {
+ return;
+ }
+
+ this.onChange(this.index, { ...this.variable, value });
+ }
+
+ hasValue() {
+ return typeof this.variable.value !== 'undefined';
+ }
+
+ removeValue() {
+ this.onChange(this.index, { name: this.variable.name });
+ }
+
+ $onInit() {
+ this.formName = `variableForm${this.index}`;
+ }
+}
+
+export default EnvironmentVariablesSimpleModeItemController;
diff --git a/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.html b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.html
new file mode 100644
index 000000000..c53af0699
--- /dev/null
+++ b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ value
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/index.js b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/index.js
new file mode 100644
index 000000000..b95b3b7db
--- /dev/null
+++ b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/index.js
@@ -0,0 +1,17 @@
+import angular from 'angular';
+import controller from './environment-variables-simple-mode-item.controller.js';
+
+export const environmentVariablesSimpleModeItem = {
+ templateUrl: './environment-variables-simple-mode-item.html',
+ controller,
+
+ bindings: {
+ variable: '<',
+ index: '<',
+
+ onChange: '<',
+ onRemove: '<',
+ },
+};
+
+angular.module('portainer.app').component('environmentVariablesSimpleModeItem', environmentVariablesSimpleModeItem);
diff --git a/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode.controller.js b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode.controller.js
new file mode 100644
index 000000000..813911832
--- /dev/null
+++ b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode.controller.js
@@ -0,0 +1,43 @@
+import { parseDotEnvFile } from '@/portainer/helpers/env-vars';
+
+export default class EnvironmentVariablesSimpleModeController {
+ /* @ngInject */
+ constructor($async) {
+ this.$async = $async;
+
+ this.onChangeVariable = this.onChangeVariable.bind(this);
+ this.remove = this.remove.bind(this);
+ }
+
+ add() {
+ this.onChange([...this.ngModel, { name: '', value: '' }]);
+ }
+
+ remove(index) {
+ this.onChange(this.ngModel.filter((_, i) => i !== index));
+ }
+
+ addFromFile(file) {
+ return this.$async(async () => {
+ if (!file) {
+ return;
+ }
+ const text = await this.getTextFromFile(file);
+ const parsed = parseDotEnvFile(text);
+ this.onChange(this.ngModel.concat(parsed));
+ });
+ }
+
+ getTextFromFile(file) {
+ return new Promise((resolve, reject) => {
+ const temporaryFileReader = new FileReader();
+ temporaryFileReader.readAsText(file);
+ temporaryFileReader.onload = (event) => resolve(event.target.result);
+ temporaryFileReader.onerror = (error) => reject(error);
+ });
+ }
+
+ onChangeVariable(index, variable) {
+ this.onChange(this.ngModel.map((v, i) => (i !== index ? v : variable)));
+ }
+}
diff --git a/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode.css b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode.css
new file mode 100644
index 000000000..155c9684a
--- /dev/null
+++ b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode.css
@@ -0,0 +1,33 @@
+.advanced-actions > * + * {
+ margin-top: 5px;
+}
+
+.environment-variables-simple-mode--actions {
+ display: flex;
+ align-items: center;
+ margin-left: 10px;
+ margin-top: 10px;
+}
+
+.env-items-list {
+ margin-top: 10px;
+}
+
+.env-items-list > * + * {
+ margin-top: 2px;
+}
+
+.env-items-list .env-item {
+ display: flex;
+}
+
+.env-item .env-item-key {
+}
+
+.env-item .env-item-value {
+ display: flex;
+}
+
+.env-item .env-item-value .input-group {
+ flex: 1;
+}
diff --git a/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode.html b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode.html
new file mode 100644
index 000000000..66a6f0fe4
--- /dev/null
+++ b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode.html
@@ -0,0 +1,36 @@
+
+
+
+
+ Switch to advanced mode to copy & paste multiple variables
+
+
+
+
+
+
+ File too large! Try uploading a file smaller than 1MB
+
+
+
+
+
+
diff --git a/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/index.js b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/index.js
new file mode 100644
index 000000000..d51f2c11d
--- /dev/null
+++ b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/index.js
@@ -0,0 +1,15 @@
+import angular from 'angular';
+
+import './environment-variables-simple-mode.css';
+
+import controller from './environment-variables-simple-mode.controller';
+
+angular.module('portainer.app').component('environmentVariablesSimpleMode', {
+ templateUrl: './environment-variables-simple-mode.html',
+ controller,
+ bindings: {
+ ngModel: '<',
+ onSwitchModeClick: '<',
+ onChange: '<',
+ },
+});
diff --git a/app/portainer/components/environment-variables-panel/index.js b/app/portainer/components/environment-variables-panel/index.js
new file mode 100644
index 000000000..9da15a489
--- /dev/null
+++ b/app/portainer/components/environment-variables-panel/index.js
@@ -0,0 +1,15 @@
+import angular from 'angular';
+
+import './environment-variables-panel.css';
+
+import controller from './environment-variables-panel.controller.js';
+
+angular.module('portainer.app').component('environmentVariablesPanel', {
+ templateUrl: './environment-variables-panel.html',
+ controller,
+ bindings: {
+ ngModel: '<',
+ explanation: '@',
+ onChange: '<',
+ },
+});
diff --git a/app/portainer/filters/filters.js b/app/portainer/filters/filters.js
index f31705979..4ed03afc7 100644
--- a/app/portainer/filters/filters.js
+++ b/app/portainer/filters/filters.js
@@ -79,12 +79,20 @@ angular
.filter('key', function () {
'use strict';
return function (pair, separator) {
+ if (!pair.includes(separator)) {
+ return pair;
+ }
+
return pair.slice(0, pair.indexOf(separator));
};
})
.filter('value', function () {
'use strict';
return function (pair, separator) {
+ if (!pair.includes(separator)) {
+ return '';
+ }
+
return pair.slice(pair.indexOf(separator) + 1);
};
})
diff --git a/app/portainer/helpers/env-vars.js b/app/portainer/helpers/env-vars.js
new file mode 100644
index 000000000..972fc5ebd
--- /dev/null
+++ b/app/portainer/helpers/env-vars.js
@@ -0,0 +1,61 @@
+import _ from 'lodash-es';
+
+export const KEY_REGEX = /[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?/.source;
+
+export const VALUE_REGEX = /(.*)?/.source;
+
+const KEY_VALUE_REGEX = new RegExp(`^(${KEY_REGEX})\\s*=(${VALUE_REGEX})$`);
+const NEWLINES_REGEX = /\n|\r|\r\n/;
+
+/**
+ * @param {string} src the source of the .env file
+ *
+ * @returns {[{name: string, value: string}]} array of {name, value}
+ */
+export function parseDotEnvFile(src) {
+ return parseArrayOfStrings(
+ _.compact(src.split(NEWLINES_REGEX))
+ .map((v) => v.trim())
+ .filter((v) => !v.startsWith('#'))
+ );
+}
+
+/**
+ * parses an array of name=value to array of {name, value}
+ *
+ * @param {[string]} array array of strings in format name=value
+ *
+ * @returns {[{name: string, value: string}]} array of {name, value}
+ */
+export function parseArrayOfStrings(array) {
+ if (!array) {
+ return [];
+ }
+
+ return _.compact(
+ array.map((variableString) => {
+ if (!variableString.includes('=')) {
+ return { name: variableString };
+ }
+
+ const parsedKeyValArr = variableString.trim().match(KEY_VALUE_REGEX);
+ if (parsedKeyValArr != null && parsedKeyValArr.length > 4) {
+ return { name: parsedKeyValArr[1], value: parsedKeyValArr[3] || '' };
+ }
+ })
+ );
+}
+/**
+ * converts an array of {name, value} to array of `name=value`, name is always defined
+ *
+ * @param {[{name, value}]} array array of {name, value}
+ *
+ * @returns {[string]} array of `name=value`
+ */
+export function convertToArrayOfStrings(array) {
+ if (!array) {
+ return [];
+ }
+
+ return array.filter((variable) => variable.name).map(({ name, value }) => (value || value === '' ? `${name}=${value}` : name));
+}
diff --git a/app/portainer/views/stacks/create/createStackController.js b/app/portainer/views/stacks/create/createStackController.js
index 76a932e67..f632101ea 100644
--- a/app/portainer/views/stacks/create/createStackController.js
+++ b/app/portainer/views/stacks/create/createStackController.js
@@ -53,14 +53,6 @@ angular
}
};
- $scope.addEnvironmentVariable = function () {
- $scope.formValues.Env.push({ name: '', value: '' });
- };
-
- $scope.removeEnvironmentVariable = function (index) {
- $scope.formValues.Env.splice(index, 1);
- };
-
function validateForm(accessControlData, isAdmin) {
$scope.state.formValidationError = '';
var error = '';
@@ -123,6 +115,11 @@ angular
}
}
+ $scope.handleEnvVarChange = handleEnvVarChange;
+ function handleEnvVarChange(value) {
+ $scope.formValues.Env = value;
+ }
+
$scope.deployStack = function () {
var name = $scope.formValues.Name;
var method = $scope.state.Method;
diff --git a/app/portainer/views/stacks/create/createstack.html b/app/portainer/views/stacks/create/createstack.html
index 6719edfec..4edb6cf50 100644
--- a/app/portainer/views/stacks/create/createstack.html
+++ b/app/portainer/views/stacks/create/createstack.html
@@ -7,7 +7,7 @@