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

refactor(ui): migrate env var field to react [EE-4853] (#8451)

This commit is contained in:
Chaim Lev-Ari 2023-05-31 10:08:41 +07:00 committed by GitHub
parent 6b5940e00e
commit 2d05103fed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 721 additions and 442 deletions

View file

@ -1,34 +0,0 @@
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(value) {
this.editorText = value;
this.onChange(parseDotEnvFile(this.editorText));
}
}

View file

@ -1,11 +0,0 @@
.environment-variables-panel {
margin-top: 15px;
}
.environment-variables-panel--explanation {
margin-bottom: 5px;
}
.environment-variables-panel--advanced > * + * {
margin-top: 5px;
}

View file

@ -1,35 +0,0 @@
<ng-form class="form-horizontal environment-variables-panel" name="$ctrl.envVarsForm">
<div class="form-group">
<div class="col-sm-12 form-section-title" style="margin-top: 10px; margin-left: 15px; width: 98%"> Environment variables </div>
<div class="col-sm-12 environment-variables-panel--explanation">
{{::$ctrl.explanation}}
</div>
<environment-variables-simple-mode
ng-if="$ctrl.mode == 'simple'"
ng-model="$ctrl.ngModel"
on-change="($ctrl.handleSimpleChange)"
on-switch-mode-click="($ctrl.switchEnvMode)"
show-help-message="$ctrl.showHelpMessage"
></environment-variables-simple-mode>
<div ng-if="$ctrl.mode == 'advanced'" class="environment-variables-panel--advanced">
<div class="col-sm-12 text-clickable">
<button type="button" class="btn btn-link btn-sm vertical-center !ml-0 p-0 hover:no-underline" ng-click="$ctrl.switchEnvMode()">
<pr-icon icon="'list'"></pr-icon> Simple mode
</button>
</div>
<div class="col-sm-12 small text-muted">
<pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon>
Switch to simple mode to define variables line by line, or load from .env file
</div>
<div class="col-sm-12">
<code-editor identifier="environment-variables-editor" placeholder="e.g. key=value" value="$ctrl.editorText" on-change="($ctrl.editorUpdate)"></code-editor>
</div>
<div class="col-sm-12 small text-muted" ng-if="$ctrl.showHelpMessage">
<pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon>
Environment changes will not take effect until redeployment occurs manually or via webhook.
</div>
</div>
</div>
</ng-form>

View file

@ -1,25 +0,0 @@
class EnvironmentVariablesSimpleModeItemController {
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 });
}
$onInit() {
this.formName = `variableForm${this.index}`;
}
}
export default EnvironmentVariablesSimpleModeItemController;

View file

@ -1,59 +0,0 @@
<ng-form class="env-item mt-1" name="$ctrl.{{ $ctrl.formName }}">
<div class="col-sm-5">
<div class="input-group input-group-sm env-item-key w-full">
<span class="input-group-addon">name</span>
<input
type="text"
name="name"
class="form-control"
placeholder="e.g. FOO"
ng-model="$ctrl.variable.name"
ng-disabled="$ctrl.variable.added"
ng-change="$ctrl.onChangeName($ctrl.variable.name)"
required
/>
</div>
<div class="form-group" style="margin-top: 5px" ng-show="$ctrl[$ctrl.formName].name.$invalid">
<div class="col-sm-12 small">
<div ng-messages="$ctrl[$ctrl.formName].name.$error">
<p ng-message="required"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Name is required. </p>
<p ng-message="pattern">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
This field must consist alphanumeric characters, '-' or '_', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').
</p>
</div>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="env-item-value w-full">
<div class="input-group input-group-sm">
<span class="input-group-addon">value</span>
<input
type="text"
class="form-control"
ng-model="$ctrl.variable.value"
placeholder="e.g. bar"
ng-trim="false"
name="value"
ng-change="$ctrl.onChangeValue($ctrl.variable.value)"
/>
</div>
</div>
<div class="form-group" ng-show="$ctrl[$ctrl.formName].value.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="$ctrl[$ctrl.formName].value.$error">
<p ng-message="pattern">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
Value is invalid.
</p>
</div>
</div>
</div>
</div>
<div>
<button class="btn btn-dangerlight" type="button" ng-click="$ctrl.onRemove($ctrl.index)">
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
</div>
</ng-form>

View file

@ -1,17 +0,0 @@
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);

View file

@ -1,43 +0,0 @@
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)));
}
}

View file

@ -1,33 +0,0 @@
.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: 10px 0;
}
.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;
}

View file

@ -1,40 +0,0 @@
<div class="environment-variables-simple-mode">
<div class="col-sm-12">
<button type="button" class="btn btn-link btn-sm !ml-0 p-0 hover:no-underline" ng-click="$ctrl.onSwitchModeClick()"> <pr-icon icon="'edit'"></pr-icon> Advanced mode </button>
</div>
<div class="col-sm-12 small text-muted">
<pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon>
Switch to advanced mode to copy & paste multiple variables
</div>
<div class="col-sm-12 form-inline env-items-list">
<environment-variables-simple-mode-item
ng-repeat="variable in $ctrl.ngModel"
variable="variable"
index="$index"
on-change="($ctrl.onChangeVariable)"
on-remove="($ctrl.remove)"
></environment-variables-simple-mode-item>
</div>
<div class="col-sm-12 environment-variables-simple-mode--actions">
<button type="button" class="btn btn-sm btn-default" ng-click="$ctrl.add()"> <pr-icon icon="'plus'"></pr-icon> Add an environment variable </button>
<button
type="button"
class="btn btn-sm btn-default"
ngf-select="$ctrl.addFromFile($file)"
ngf-accept="'.env'"
ngf-pattern="'.env'"
ngf-max-size="1MB"
ngf-model-invalid="errorFile"
>
<pr-icon icon="'upload'"></pr-icon> Load variables from .env file
</button>
<span class="space-left" ng-if="errorFile.$error == 'maxSize'">
<pr-icon icon="'x'" mode="'warning'" class-name="'space-right'"></pr-icon>
File too large! Try uploading a file smaller than 1MB
</span>
</div>
<div class="col-sm-12 small text-muted" ng-if="$ctrl.ngModel.length > 0 && $ctrl.showHelpMessage">
<pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon>
Environment changes will not take effect until redeployment occurs manually or via webhook.
</div>
</div>

View file

@ -1,16 +0,0 @@
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: '<',
showHelpMessage: '<',
},
});

View file

@ -1,16 +0,0 @@
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: '<',
showHelpMessage: '<',
},
});

View file

@ -62,8 +62,8 @@
</div>
<environment-variables-panel
ng-model="$ctrl.formValues.Env"
explanation="These values will be used as substitutions in the stack file. To reference the .env file in your compose file, use stack.env."
values="$ctrl.formValues.Env"
explanation="'These values will be used as substitutions in the stack file. To reference the .env file in your compose file, use stack.env.'"
on-change="($ctrl.onChangeEnvVar)"
show-help-message="true"
></environment-variables-panel>

View file

@ -1,60 +0,0 @@
import _ from 'lodash-es';
export const KEY_REGEX = /(.+?)/.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('#') && v !== '')
);
}
/**
* 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].trim(), value: parsedKeyValArr[3].trim() || '' };
}
})
);
}
/**
* 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));
}

View file

@ -9,4 +9,5 @@ export const fileUploadField = r2a(FileUploadField, [
'required',
'accept',
'inputId',
'color',
]);

View file

@ -5,7 +5,13 @@ import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { withReactQuery } from '@/react-tools/withReactQuery';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { AnnotationsBeTeaser } from '@/react/kubernetes/annotations/AnnotationsBeTeaser';
import { withFormValidation } from '@/react-tools/withFormValidation';
import {
EnvironmentVariablesFieldset,
EnvironmentVariablesPanel,
envVarValidation,
} from '@@/form-components/EnvironmentVariablesFieldset';
import { Icon } from '@@/Icon';
import { ReactQueryDevtoolsWrapper } from '@@/ReactQueryDevtoolsWrapper';
import { PageHeader } from '@@/PageHeader';
@ -37,7 +43,7 @@ import { environmentsModule } from './environments';
import { envListModule } from './environments-list-view-components';
import { registriesModule } from './registries';
export const componentsModule = angular
export const ngModule = angular
.module('portainer.app.react.components', [
accessControlModule,
customTemplatesModule,
@ -197,4 +203,22 @@ export const componentsModule = angular
'height',
])
)
.component('annotationsBeTeaser', r2a(AnnotationsBeTeaser, [])).name;
.component('annotationsBeTeaser', r2a(AnnotationsBeTeaser, []));
export const componentsModule = ngModule.name;
withFormValidation(
ngModule,
EnvironmentVariablesFieldset,
'environmentVariablesFieldset',
[],
envVarValidation
);
withFormValidation(
ngModule,
EnvironmentVariablesPanel,
'environmentVariablesPanel',
['explanation', 'showHelpMessage'],
envVarValidation
);

View file

@ -0,0 +1,25 @@
export function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer | null> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = () => {
if (reader.result instanceof ArrayBuffer) {
resolve(reader.result);
}
};
reader.onerror = (error) => reject(error);
});
}
export function readFileAsText(file: File): Promise<string | null> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsText(file);
reader.onload = () => {
if (typeof reader.result === 'string') {
resolve(reader.result);
}
};
reader.onerror = (error) => reject(error);
});
}

View file

@ -153,8 +153,8 @@
<!-- environment-variables -->
<environment-variables-panel
ng-model="formValues.Env"
explanation="These values will be used as substitutions in the stack file. To reference the .env file in your compose file, use stack.env"
values="formValues.Env"
explanation="'These values will be used as substitutions in the stack file. To reference the .env file in your compose file, use stack.env'"
on-change="(handleEnvVarChange)"
>
</environment-variables-panel>

View file

@ -181,8 +181,8 @@
<!-- environment-variables -->
<div ng-if="stack">
<environment-variables-panel
ng-model="formValues.Env"
explanation="These values will be used as substitutions in the stack file. To reference the .env file in your compose file, use stack.env."
values="formValues.Env"
explanation="'These values will be used as substitutions in the stack file. To reference the .env file in your compose file, use stack.env.'"
on-change="(handleEnvVarChange)"
show-help-message="true"
></environment-variables-panel>